编写高质量代码:改善Java程序的151个建议(第7章:泛型和反射___建议98~101)
建议98:建议的采用顺序是List中泛型顺序依次为T、?、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类是“类中类”,也就有预示着它有很多特殊的地方:
- 无构造函数:Java中的类一般都有构造函数,用于创建实例对象,但是Class类却没有构造函数,不能实例化,Class对象是在加载类时由Java虚拟机通过调用类加载器中的difineClass方法自动构造的。
- 可以描述基本类型:虽然8个基本类型在JVM中并不是一个对象,它们一般存在于栈内存中,但是Class类仍然可以描述它们,例如可以使用int.class表示int类型的类对象。
- 其对象都是单例模式:一个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对象有三种途径:
- 类属性方式:如String.class
- 对象的getClass方法,如new String().getClass()
- forName方法加载:如Class.forName(" java.lang.String")
获得了Class对象后,就可以通过getAnnotations()获得注解,通过getMethods()获得方法,通过getConstructors()获得构造函数等,这位后续的反射代码铺平了道路。