学习笔记之Java泛型
2016年4月21日, 发表于 长春
如果你对本文有任何的建议或者疑问, 可以在 这里给我提 Issues, 谢谢! :)
有限制的泛型–关于擦除
Java SE5开始支持泛型,由于兼容性的原因,采用了擦除的方法,这导致了很多问题。本文是我的学习笔记,不对之处希望指出!
1.泛型的Class类型
1 2 | Class c1 = new ArrayList<String>().getClass(); Class c2 = new ArrayList<Integer>().getClass(); |
此时的c1==c2是成立的,它们都为ArrayList.class即Class< ArrayList >的对象(单例),因为类型被擦除了。
2.泛型的参数标识符
字节码是没有泛型这一概念的,java文件编译成字节码时,泛型参数类型被擦除,替换为它们的非泛型上界(举个例子,若默认边界,即占位符T被替换为Object)。
此外,在泛型代码内部,无法获得任何有关泛型参数的信息。例: getTypeParameters()这个函数将返回一个TypeVariable数组,表示有泛型声明所声明的类型参数,但实际上,它返回的仅仅是用作参数占位符的标识符,例如定义一个类时
1 | class MyClass<T>{} |
getTypeParameters()将返回[T]。
3.泛型数组与泛型的实例化
(1)不能直接创建带泛型参数化的数组。
原因是泛型数组会破坏类型安全。 举个例子,Object[]数组是任何数组的父类,任何一个数组都可以向上转型成Object[],假设原始数组为String[],我把它转型为Object[],现在往里添加一个Integer元素,编译不会有问题,因为Integer也说Object的子类,但是在运行时会检查加入数组的对象的类型,于是会抛ArrayStoreException。:
1 2 3 | String[] strArray = new String[20]; Object[] objArray = strArray; objArray[0] = new Integer(1); //ArrayStoreException |
现在我们开始考虑泛型
1 2 3 4 5 | Map<Integer, String>[] mapArray = new HashMap<Integer, String>[20];//ERROR Map<Object,Object>[] mapArray2 = mapArray; Map<Integer, Double> errorMap = new HashMap<Integer, Double>(); error.put(1,1.0); mapArray2[0] = errorMap;//检测不出错误 |
这样不但编译器不能发现类型错误,就连运行时的数组存储检查对它也无能为力,因为泛型的类型擦除,我们定义的String已经被擦除了,结果我们却可以往里面放任何Map,接下来如果有代码试图按原有的定义去取值,结果会是灾难性的,所以索性不允许创建带有泛型参数化的数组。 数组必须牢记它的元素类型,也就是所有的元素对象都必须一个样,泛型类型数组做不到。
(2)任何在运行时需要知道确切类型信息的操作都无法工作。
1 2 3 4 5 6 7 8 9 10 | //代码摘自Thinging in Java public class Erased<T>{ private final int SIZE = 100; public static void f(Object arg){ if(arg instance of T){}//Error 1 T var = new T()//Error 2 T[] array = new T[SIZE]//Error 3 T[] array = (T)new Object[Size]//Error 4 } } |
个人拙见,原因可能是:1.由于擦除,编译器不知道T的具体类型,如果要new或其他需要知道确切类型信息的操作,只能将T擦除到边界处,在这即Object,而我们在使用泛型类的时候,编译器自动将Object转型为我们需要的类型,但是,由于我们new(或其他操作)的是Object,就会发生类型转换错误,这是不被允许的。2.不能实例化的另一原因是因为编译器不能验证T具有默认无参构造器。 解决方案: ERROR 1:可以采用动态的isInstance(),定义一个Class< T > kind,然后用kind.isInstance(arg)判断。 ERROR 2:思路是传递一个工厂对象,来创建新实例,例如Class对象的newInstance(),如果想要的对象没有默认无参构造器,需要用反射的方法获得构造器进行实例化。 ERROR 3和4:最简单的方法是使用ArrayList,如果仍旧希望创建数组,可以使用java.lang.reflect中的Array.newInstance(),例子:
1 2 3 4 5 6 7 8 9 10 | import java.lang.reflect.*; public class GenericArray<T>{ private T[] array; @suppressWarnings("unchecked") public GenericArray(Class<T> type,int size){ array = (T[])Array.newInstance(type,size); } /*** ***/ } |
4.泛型与重载
我们已经知道泛型会有擦除,所以List
1 2 3 4 5 6 7 8 9 10 | public class GenericTypes { public static void method(List<String> list) { System.out.println("List<String> list"); } public static void method(List<Integer> list) { System.out.println("List<Integer> list"); } } |
JVM不能识别出这两个方法的区别。
5.静态变量
由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,不管实例化的参数是哪种类型,所有静态变量是共享的。
6.关于通配符
(1)泛型与通配符的继承关系
1 2 | class Father{} class Son extends Father{} |
在这里Father是Son的超类,但是ArrayList
(2)通配符的协变与逆变
1 2 3 4 5 6 7 8 9 | class Son2 extends Father{} List<? extends Father> fatherList = new ArrayList<Son>(); //Compile Error //fatherList.add(new Son()); //fatherList.add(new Son2()); fatherList.add(null);//OK Father f = fatherList.get(0);//OK fatherList.contains(new Son());//OK fatherList.indexOf(new Son());//OK |
查看ArrayList的文档,可知原因在于add()的参数是一个具有泛型参数类型的参数,在这里就是? extends Father,而编译器不知道到底需要Father的哪个子类型,所以它不会接受任何类型的参数(null除外)。而contains()和indexOf()的参数为Object,因此允许此调用。我们平时设计泛型类时也可以参考这种方法。 而与之相反的超类型通配符<? super Son>则不同:
1 2 3 4 5 | class GrandSon extends Son{} List<? super Son> sonList = new ArrayList<Son>(); sonList.add(new Son()); sonList.add(new GrandSon()); //sonList.add(new Father()); //ERROR |
由于sonList是Son的某种基类的list,根据向上转型的规则,往里添加任何SOn或者Son的子类都是可以的,但是,往sonList里添加Father是不安全的,因为编译器不知道sonList到底是Son的哪种基类的List。
7.一个类不能实现同一个泛型接口的两种变体
1 2 3 4 5 6 7 | interface Payable{void method(){}} class Employee implements Payable{} class Hourly extends Employee implements Payable{} interface Payable2<T>{} class Employee2 implements Payable2<Employee2>{} class Hourly2 extends Employee2 implements Payable2<Hourly2>{}//ERROR |
这是Thingking in java的内容,但是并没有讲详细,为什么前者可通过编译,而后者不可以?
第一个普通接口编译通过,因为即使继承的类实现了同样的接口,也可以被覆盖。
(我自己的理解,若有错误之处,希望指出!)而对于泛型接口,由于擦除的原因,泛型参数类型被擦除到边界,在这里也就是Object,这时实现了接口的类需要一个桥方法实现覆盖(桥方法的详细内容可以谷歌),Java的编译器自动帮我们完成了这项任务,生成了一个参数类型是Object的桥方法,这个方法自动调用我们实现的方法,现在问题来了,在Hourly2中,桥方法调用的究竟是接口Payable