编写高质量代码:改善Java程序的151个建议(第7章:泛型和反射___建议98~101)

建议98:建议的采用顺序是List<T>、List<?>、List<Object>

  List<T>、List<?>、List<Object>这三者都可以容纳所有的对象,但使用的顺序应该是首选List<T>,次之List<?>,最后选择List<Object>,原因如下:

(1)、List<T>是确定的某一个类型

  List<T>表示的是List集合中的元素都为T类型,具体类型在运行期决定;List<?>表示的是任意类型,与List<T>类似,而List<Object>则表示List集合中的所有元素为Object类型,因为Object是所有类的父类,所以List<Object>也可以容纳所有的类类型,从这一字面意义上分析,List<T>更符合习惯:编码者知道它是某一个类型,只是在运行期才确定而已。

(2)List<T>可以进行读写操作

  List<T>可以进行诸如add,remove等操作,因为它的类型是固定的T类型,在编码期不需要进行任何的转型操作。

  List<T>是只读类型的,不能进行增加、修改操作,因为编译器不知道List中容纳的是什么类型的元素,也就无法校验类型是否安全了,而且List<?>读取出的元素都是Object类型的,需要主动转型,所以它经常用于泛型方法的返回值。注意List<?>虽然无法增加,修改元素,但是却可以删除元素,比如执行remove、clear等方法,那是因为它的删除动作与泛型类型无关。

  List<Object> 也可以读写操作,但是它执行写入操作时需要向上转型(Up cast),在读取数据的时候需要向下转型,而此时已经失去了泛型存在的意义了。

  打个比方,有一个篮子用来容纳物品,比如西瓜,番茄等.List<?>的意思是说,“嘿,我这里有一个篮子,可以容纳固定类别的东西,比如西瓜,番茄等”。List<?>的意思是说:“嘿,我有一个篮子,我可以容纳任何东西,只要是你想得到的”。而List<Object>就更有意思了,它说" 嘿,我也有一个篮子,我可以容纳所有物质,只要你认为是物质的东西都可以容纳进来 "。

  推而广之,Dao<T>应该比Dao<?>、Dao<Object>更先采用,Desc<Person>则比Desc<?>、Desc<Object>更优先采用。

建议99:严格限定泛型类型采用多重界限

  从哲学来说,很难描述一个具体的人,你可以描述他的长相、性格、工作等,但是人都是由多重身份的,估计只有使用多个And(与操作)将所有的描述串联起来才能描述一个完整的人,比如我,上班时我是一个职员,下班了坐公交车我是一个乘客,回家了我是父母的孩子,是儿子的父亲......角色时刻在变换。那如果我们要使用Java程序来对一类人进行管理,该如何做呢?比如在公交车费优惠系统中,对部分人员(如工资低于2500元的上班族并且是站立的乘客)车费打8折,该如何实现呢?

  注意这里的类型参数有两个限制条件:一个为上班族;二为乘客。具体到我们的程序中就应该是一个泛型参数具有两个上界(Upper Bound),首先定义两个接口及实现类,代码如下: 

 1 interface Staff {
 2     // 工资
 3     public int getSalary();
 4 }
 5
 6 interface Passenger {
 7     // 是否是站立状态
 8     public boolean isStanding();
 9 }
10 //定义我这个类型的人
11 class Me implements Staff, Passenger {
12
13     @Override
14     public boolean isStanding() {
15         return true;
16     }
17
18     @Override
19     public int getSalary() {
20         return 2000;
21     }
22
23 }

  "Me"这种类型的人物有很多,比如系统分析师也是一个职员,也坐公交车,但他的工资实现就和我不同,再比如Boss级的人物,偶尔也坐公交车,对大老板来说他也只是一个职员,他的实现类也不同,也就是说如果我们使用“T extends Me”是限定不了需求对象的,那该怎么办呢?可以考虑使用多重限定,代码如下:  

public class Client99 {
    //工资低于2500的并且站立的乘客车票打8折
    public static <T extends Staff & Passenger> void discount(T t) {
        if (t.getSalary() < 2500 && t.isStanding()) {
            System.out.println(" 恭喜您,您的车票打八折!");
        }
    }
    public static void main(String[] args) {
        discount(new Me());
    }
}

  使用“&”符号设定多重边界,指定泛型类型T必须是Staff和Passenger的共有子类型,此时变量t就具有了所有限定的方法和属性,要再进行判断就一如反掌了。在Java的泛型中,可以使用"&"符号关联多个上界并实现多个边界限定,而且只有上界才有此限定,下界没有多重限定的情况。想想你就会明白:多个下界,编码者可自行推断出具体的类型,比如“? super Integer” 和 “? extends Double”,可以更细化为Number类型了,或者Object类型了,无需编译器推断了。

  为什么要说明多重边界?是因为编码者太少使用它了,比如一个判断用户权限的方法,使用的是策略模式(Strategy Pattern) ,示意代码如下:

 1 class UserHandler<T extends User> {
 2     // 判断用户是否有权限执行操作
 3     public boolean permit(T user, List<Job> jobs) {
 4         List<Class<?>> iList = Arrays.asList(user.getClass().getInterfaces());
 5         // 判断 是否是管理员
 6         if (iList.indexOf(Admin.class) > -1) {
 7             Admin admin = (Admin) user;
 8             // 判断管理员是否有此权限
 9         } else {
10             // 判断普通用户是否有此权限
11         }
12         return false;
13     }
14 }
15
16 class User {}
17
18 class Job {}
19
20 class Admin extends User {}

  此处进行了一次泛型参数类别判断,这里不仅仅违背了单一职责原则(Single Responsibility Principle),而且让泛型很“汗颜” :已经使用了泛型限定参数的边界了,还要进行泛型类型判断。事实上,使用多重边界可以很方便的解决此问题,而且非常优雅,建议大家 在开发中考虑使用多重限定。

建议100:数组的真实类型必须是泛型类型的子类型

  List接口的toArray方法可以把一个集合转化为数组,但是使用不方便,toArray()方法返回的是一个Object数组,所以需要自行转变。toArray(T[] a)虽然返回的是T类型的数组,但是还需要传入一个T类型的数组,这也挺麻烦的,我们期望输入的是一个泛型化的List,这样就能转化为泛型数组了,来看看能不能实现,代码如下:

    public static <T> T[] toArray(List<T> list) {
        T[] t = (T[]) new Object[list.size()];
        for (int i = 0, n = list.size(); i < n; i++) {
            t[i] = list.get(i);
        }
        return t;
    }

  上面要输出的参数类型定义为Object数组,然后转型为T类型数组,之后遍历List赋值给数组的每个元素,这与ArrayList的toArray方法很类似(注意只是类似),客户端的调用如下:

public static void main(String[] args) {
        List<String> list = Arrays.asList("A","B");
        for(String str :toArray(list)){
            System.out.println(str);
        }
    }

  编译没有任何问题,运行后出现如下异常:  

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
    at com.study.advice100.Client100.main(Client100.java:16)

  类型转换异常,也就是说不能把一个Object数组转换为String数组,这段异常包含了两个问题:

  • 为什么Object数组不能向下转型为String数组:数组是一个容器,只有确保容器内的所有元素类型与期望的类型有父子关系时才能转换,Object数组只能保证数组内的元素时Object类型,却不能确保它们都是String的父类型或子类,所以类型转换失败。
  • 为什么是main方法抛出异常,而不是toArray方法:其实,是在toArray方法中进行的类型向下转换,而不是main方法中。那为什么异常会在main方法中抛出,应该在toArray方法的“ T[] t = (T[]) new Object[list.size()];”这段代码才对呀?那是因为泛型是类型擦除的,toArray方法经过编译后与如下代码相同:  
    public static Object[] toArrayTwo(List list) {
        // 此处的强制类型转换没必要存在,只是为了与源代码对比
        Object[] t = (Object[]) new Object[list.size()];
        for (int i = 0, n = list.size(); i < n; i++) {
            t[i] = list.get(i);
        }
        return t;
    }

    public static void main(String[] args) {
        List<String> list = Arrays.asList("A", "B");
        for (String str : (String [])toArrayTwo(list)) {
            System.out.println(str);
        }
    }

  阅读完此段代码后就很清楚了:toArray方法返回后进行一次类型转换,Object数组转换成了String数组,于是就报ClassCastException异常了。

  Object数组不能转为String数组,T类型又无法在运行期获得,那该如何解决这个问题呢?其实,要想把一个Object数组转换为String数组,只要Object数组的实际类型也就是String就可以了,例如: 

       // objArray的实际类型和表面类型都是String数组
        Object[] objArray = { "A", "B" };
        // 抛出ClassCastException
        String[] strArray = (String[]) objArray;

        String[] ss = { "A", "B" };
        //objs的真实类型是String数组,显示类型为Object数组
        Object objs[] =ss;
        //顺利转换为String数组
        String strs[]=(String[])objs;

  明白了这个问题,我们就把泛型数组声明为泛型的子类型吧!代码如下:

    public static <T> T[] toArray(List<T> list,Class<T> tClass) {
        //声明并初始化一个T类型的数组
        T[] t = (T[])Array.newInstance(tClass, list.size());
        for (int i = 0, n = list.size(); i < n; i++) {
            t[i] = list.get(i);
        }
        return t;
    }

  通过反射类Array声明了一个T类型的数组,由于我们无法在运行期获得泛型类型的参数,因此就需要调用者主动传入T参数类型。此时,客户端再调用就不会出现任何异常了。

  在这里我们看到,当一个泛型类(特别是泛型集合)转变为泛型数组时,泛型数组的真实类型不能是泛型的父类型(比如顶层类Object),只能是泛型类型的子类型(当然包括自身类型),否则就会出现类型转换异常。

建议101:注意Class类的特殊性

  Java语言是先把Java源文件编译成后缀为class的字节码文件,然后再通过ClassLoader机制把这些类文件加载到内存中,最后生成实例执行的,这是Java处理的基本机制,但是加载到内存中的数据的如何描述一个类的呢?比如在Dog.class文件中定义一个Dog类,那它在内存中是如何展现的呢?

  Java使用一个元类(MetaClass)来描述加载到内存中的类数据,这就是Class类,它是一个描述类的类对象,比如Dog.class文件加载到内存中后就会有一个class的实例对象描述之。因为是Class类是“类中类”,也就有预示着它有很多特殊的地方:

  1. 无构造函数:Java中的类一般都有构造函数,用于创建实例对象,但是Class类却没有构造函数,不能实例化,Class对象是在加载类时由Java虚拟机通过调用类加载器中的difineClass方法自动构造的。
  2. 可以描述基本类型:虽然8个基本类型在JVM中并不是一个对象,它们一般存在于栈内存中,但是Class类仍然可以描述它们,例如可以使用int.class表示int类型的类对象。
  3. 其对象都是单例模式:一个Class的实例对象描述一个类,并且只描述一个类,反过来也成立。一个类只有一个Class实例对象,如下代码返回的结果都为true: 
        // 类的属性class所引用的对象与实例对象的getClass返回值相同
        boolean b1=String.class.equals(new String().getClass());
        boolean b2="ABC".getClass().equals(String.class);
        // class实例对象不区分泛型
        boolean b3=ArrayList.class.equals(new ArrayList<String>().getClass());

  Class类是Java的反射入口,只有在获得了一个类的描述对象后才能动态的加载、调用,一般获得一个Class对象有三种途径:

  1. 类属性方式:如String.class
  2. 对象的getClass方法,如new String().getClass()
  3. forName方法加载:如Class.forName(" java.lang.String")

  获得了Class对象后,就可以通过getAnnotations()获得注解,通过getMethods()获得方法,通过getConstructors()获得构造函数等,这位后续的反射代码铺平了道路。

时间: 2024-10-12 10:03:05

编写高质量代码:改善Java程序的151个建议(第7章:泛型和反射___建议98~101)的相关文章

编写高质量代码改善java程序的151个建议——导航开篇

2014-05-16 09:08 by Jeff Li 前言 系列文章:[传送门] 下个星期度过这几天的奋战,会抓紧java的进阶学习.听过一句话,大哥说过,你一个月前的代码去看下,惨不忍睹是吧.确实,人和代码一样都在成长,都在变好当中.有时候只是实现功能的编程,长进不了呀. 博客提供的好处就可以交流,讨论的学习方法你们应该知道. 在这里,我会陆陆续续的进行对<编写高质量代码改善java程序的151个建议>看法,希望大家点击交流. 正文 看这本书原因   1.项目做的只是实现功能,然而没有好好

编写高质量代码改善java程序的151个建议——[1-3]基础?亦是基础

原创地址:   http://www.cnblogs.com/Alandre/  (泥沙砖瓦浆木匠),需要转载的,保留下! Thanks The reasonable man adapts himself to the world;the unreasonable one persists in trying to adapt the world to himself. -萧伯纳 相信自己看得懂就看得懂了,相信自己能写下去,我就开始写了.其实也简单-泥沙砖瓦浆木匠 Written In The

转载-------编写高质量代码:改善Java程序的151个建议(第1章:JAVA开发中通用的方法和准则___建议1~5)

阅读目录 建议1:不要在常量和变量中出现易混淆的字母 建议2:莫让常量蜕变成变量 建议3:三元操作符的类型务必一致 建议4:避免带有变长参数的方法重载 建议5:别让null值和空值威胁到变长方法              The reasonable man adapts himself to the world; The unreasonable one persists in trying to adapt the world himself. 明白事理的人使自己适应世界:不明事理的人想让世

编写高质量代码改善java程序的151个建议——[110-117]异常及Web项目中异常处理

原创地址:http://www.cnblogs.com/Alandre/(泥沙砖瓦浆木匠),需要转载的,保留下! 文章宗旨:Talk is cheap show me the code. 大成若缺,其用不弊.大盈若冲,其用不穷.  <道德经-老子>最完满的东西,好似有残缺一样,但它的作用永远不会衰竭:最充盈的东西,好似是空虚一样,但是它的作用是不会穷尽的 Written In The Font 摘要: 异常处理概述 学习内容: 建议110: 提倡异常封装 建议111: 采用异常链传递异常 建议

编写高质量代码:改善Java程序的151个建议 --[52~64]

编写高质量代码:改善Java程序的151个建议 --[52~64] 推荐使用String直接量赋值 Java为了避免在一个系统中大量产生String对象(为什么会大量产生,因为String字符串是程序中最经常使用的类型),于是就设计了一个字符串池(也叫作字符串常量池,String pool或String Constant Pool或String Literal Pool),在字符串池中容纳的都是String字符串对象,它的创建机制是这样的:创建一个字符串时,首先检查池中是否有字面值相等的字符串,

转载--编写高质量代码:改善Java程序的151个建议(第1章:JAVA开发中通用的方法和准则___建议16~20)

阅读目录 建议16:易变业务使用脚本语言编写 建议17:慎用动态编译 建议18:避免instanceof非预期结果 建议19:断言绝对不是鸡肋 建议20:不要只替换一个类 回到顶部 建议16:易变业务使用脚本语言编写 Java世界一直在遭受着异种语言的入侵,比如PHP,Ruby,Groovy.Javascript等,这些入侵者都有一个共同特征:全是同一类语言-----脚本语言,它们都是在运行期解释执行的.为什么Java这种强编译型语言会需要这些脚本语言呢?那是因为脚本语言的三大特征,如下所示:

编写高质量代码:改善Java程序的151个建议(第1章:JAVA开发中通用的方法和准则___建议11~15)

建议11:养成良好习惯,显示声明UID 我们编写一个实现了Serializable接口(序列化标志接口)的类,Eclipse马上就会给一个黄色警告:需要添加一个Serial Version ID.为什么要增加?他是怎么计算出来的?有什么用?下面就来解释该问题. 类实现Serializable接口的目的是为了可持久化,比如网络传输或本地存储,为系统的分布和异构部署提供先决条件支持.若没有序列化,现在我们熟悉的远程调用.对象数据库都不可能存在,我们来看一个简单的序列化类: 1 import java

转载---编写高质量代码:改善Java程序的151个建议(第3章:类、对象及方法___建议47~51)

阅读目录 建议47:在equals中使用getClass进行类型判断 建议48:覆写equals方法必须覆写hashCode方法 建议49:推荐覆写toString方法 建议50:使用package-info类为包服务 建议51:不要主动进行垃圾回收 回到顶部 建议47:在equals中使用getClass进行类型判断 本节我们继续讨论覆写equals的问题,这次我们编写一个员工Employee类继承Person类,这很正常,员工也是人嘛,而且在JavaBean中继承也很多见,代码如下: 1 p

转载---编写高质量代码:改善Java程序的151个建议(第2章:基本类型___建议21~25)

阅读目录 建议21:用偶判断,不用奇判断 建议22:用整数类型处理货币 建议23:不要让类型默默转换 建议24:边界还是边界 建议25:不要让四舍五入亏了一方 不积跬步,无以至千里: 不积小流,无以成江海. ---荀子<劝学篇> 回到顶部 建议21:用偶判断,不用奇判断 判断一个数是奇数还是偶数是小学里的基本知识,能够被2整除的整数是偶数,不能被2整除的数是奇数,这规则简单明了,还有什么可考虑的?好,我们来看一个例子,代码如下: 1 import java.util.Scanner; 2 3

编写高质量代码:改善Java程序的151个建议(第3章:类、对象及方法___建议36~40)

建议36:使用构造代码块精简程序 什么叫做代码块(Code Block)?用大括号把多行代码封装在一起,形成一个独立的数据体,实现特定算法的代码集合即为代码块,一般来说代码快不能单独运行的,必须要有运行主体.在Java中一共有四种类型的代码块: 普通代码块:就是在方法后面使用"{}"括起来的代码片段,它不能单独运行,必须通过方法名调用执行: 静态代码块:在类中使用static修饰,并用"{}"括起来的代码片段,用于静态变量初始化或对象创建前的环境初始化. 同步代码块