Java泛型下――万恶的擦除(三)
上面的分析我们可以看出Java的泛型类或者泛型方法中,对于传入的类型参数的类型信息是完全丢失的,是被擦除掉的,我们在里面连个new都办不到,这时候我们就可以利用Java的RTTI即运行时类型信息(后续博文)来解决,如下:
复制代码
1 class Building {}
2 class House extends Building {}
3 public class ClassTypeCapture {
4 Class kind;
5 T t;
6 public ClassTypeCapture(Class kind) {
7 this.kind = kind;
8 }
9 public boolean f(Object arg) {
10 return kind.isInstance(arg);
11 }
12 public void newT(){
13 t = kind.newInstance();
14 }
15 public static void main(String[] args) {
16 ClassTypeCapture ctt1 =
17 new ClassTypeCapture(Building.class);
18 System.out.println(ctt1.f(new Building()));
19 System.out.println(ctt1.f(new House()));
20 ClassTypeCapture ctt2 =
21 new ClassTypeCapture(House.class);
22 System.out.println(ctt2.f(new Building()));
23 System.out.println(ctt2.f(new House()));
24 }
25 }/* Output:
26 true
27 false
28 true
29 *///:~
复制代码
在前面的例子中我们利用instanceof来判断类型失败,因为泛型中类型信息已经被擦除了,代码第10行这里我们使用动态的isInstance(),并且传入类型标签Class这样的话我们只要在声明泛型类时,利用构造函数将它的Class类型信息传入到泛化类中,这样就补偿擦除问题
而在代码第13行这里我们同样可利用工厂对象Class对象来通过newInstance()方法得到一个T类型的实例。(这在C++中完全可以利用t = new T();实现,但是Java中丢失了类型信息,我无法知道T类型是否拥有无参构造函数)
(上面提到的Class、isInstance(),newInstance()等Java中类型信息的相关后续博文中我自己再总结)
解决方案3:
在解决方案1中我们提到了,利用边界来解决Java对泛型的类型擦除问题。就是我们声明一个接口,然后在声明泛化类或者泛化方法的时候,显示的告诉编译器其中Interface是我们任意声明的一个接口,这样在内部我们就能够知道T拥有哪些方法和T的部分类型信息。
四、通配符之协变、逆变
在使用Java中的容器的时候,我们经常会遇到类似List< extends Fruit>这种声明,这里问号 就是通配符。Fruit是一个水果类型基类,它的导出类型有Apple、Orange等等。
协变:
复制代码
1 class Fruit {}
2 class Apple extends Fruit {}
3 class Jonathan extends Apple {}
4 class Orange extends Fruit {}
5 public class CovariantArrays {
6 public static void main(String[] args) {
7 Fruit[] fruit = new Apple[10];
8 fruit[0] = new Apple(); // OK
9 fruit[1] = new Jonathan(); // OK
10 // Runtime type is Apple[], not Fruit[] or Orange[]:
11 try {
12 // Compiler allows you to add Fruit:
13 fruit[0] = new Fruit(); // ArrayStoreException
14 } catch(Exception e) { System.out.println(e); }
15 try {
16 // Compiler allows you to add Oranges:
17 fruit[0] = new Orange(); // ArrayStoreException
18 } catch(Exception e) { System.out.println(e); }
19 }
20 } /* Output:
21 java.lang.ArrayStoreException: Fruit
22 java.lang.ArrayStoreException: Orange
23 *///:~
复制代码
首先我们观察一下数组当中的协变(协变就是子类型可以被当作基类型使用),Java数组是支持协变的。如上述代码,我们会发现声明的一个Apple数组用Fruit引用来存储,但是当我们往里添加元素的时候我们只能添加Apple对象及其子类型的对象,如果试图添加别的Fruit的子类型如Orange,那么在编译器就会报错,这是非常合理的,一个Apple类型的数组很明显不能放Orange进去;但是在代码13行我们会发现,如果想要将Fruit基类型的对象放入,编译器是允许的,因为我们的数组引用是Fruit类型的,但是在运行时编译器会发现实际上Fruit引用处理的是一个Apple数组,这是就会抛出异常。
然而我们把数组的这个操作翻译到List上去,如下:
复制代码
1 public class GenericsAndCovariance {
2 public static void main(String[] args) {
3 // Wildcards allow covariance:
4 List< extends Fruit> flist = new ArrayList();
5 // Compile Error: can’t add any type of object:
6 // flist.add(new Apple());
7 // flist.add(new Fruit());
8 // flist.add(new Object());
9 flist.add(null); // Legal but uninteresting
1