Java中的泛型是伪泛型
泛型思想最早在C++语言的模板(Templates)中产生,Java后来也借用了这种思想。虽然思想一致,但是他们存在着本质性的不同。C++中的模板是真正意义上的泛型,在编译时就将不同模板类型参数编译成对应不同的目标代码,ClassName和ClassName是两种不同的类型,这种泛型被称为真正泛型。这种泛型实现方式,会导致类型膨胀,因为要为不同具体参数生成不同的类。
Java中ClassName和ClassName虽然在源代码中属于不同的类,但是编译后的字节码中,他们都被替换成原始类型(ClassName),而两者的原始类型的一样的,所以在运行时环境中,ClassName和ClassName就是同一个类。Java中的泛型是一种特殊的语法糖,通过类型擦除实现(后面介绍),这种泛型称为伪泛型。由于Java中有这么一个障眼法,如果没有进行深入研究,就会在产生莫名其妙的问题。值得一提的是,不少大牛对Java的泛型的实现方式很不满意。
类型擦除
Java中的泛型是通过类型擦除来实现的。所谓类型擦除,是指通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。
下面通过两个例子来证明在编译时确实发生了类型擦除。
例1分别创建实际类型为String和Integer的ArrayList对象,通过getClass()方法获取两个实例的类,最后判断这个实例的类是相等的,证明两个实例共享同一个类。
// 声明一个具体类型为String的ArrayList
ArrayList<String> arrayList1 = new ArrayList<String>();
arrayList1.add("abc");
// 声明一个具体类型为Integer的ArrayList
ArrayList<Integer> arrayList2 = new ArrayList<Integer>();
arrayList2.add(123);
System.out.println(arrayList1.getClass() == arrayList2.getClass()); // 结果为true
例2创建一个只能存储Integer的ArrayList对象,在add一个整型数值后,利用反射调用add(Object o)add一个asd字符串,此时运行代码不会报错,运行结果会打印出1和asd两个值。这时再里利用反射调用add(Integer o)方法,运行会抛出codeNoSuchMethodException异常。这充分证明了在编译后,擦除了Integer这个泛型信息,只保留了原始类型。
ArrayList<Integer> arrayList3 = new ArrayList<Integer>();
arrayList3.add(1);
arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
for (int i = 0; i < arrayList3.size(); i++) {
System.out.println(arrayList3.get(i)); // 输出1,asd
}
arrayList3.getClass().getMethod("add", Integer.class).invoke(arrayList3, 2); // NoSuchMethodException:java.util.ArrayList.add(java.lang.Integer)
自动类型转换
上一节上说到了类型擦除,Java编译器会擦除掉泛型信息。那么调用ArrayList的get()最终返回的必然会是一个Object对象,但是我们在源代码并没有写过Object转成Integer的代码,为什么就能“直接”将取出来的对象赋予一个Integer类型的变量呢(如下面的代码第12行)?
import java.util.List;
import java.util.ArrayList;
/**
* 泛型中的类型转换测试。
*/
public class Test {
public static void main(String[] args) {
List<Integer> a = new ArrayList<Integer>();
a.add(1);
Integer ai = a.get(0);
}
}
实际上,Java的泛型除了类型擦除之外,还会自动生成checkcast指令进行强制类型转换。上面的代码中的main方法编译后所对应的字节码如下。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: iconst_1
10: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
13: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
18: pop
19: aload_1
20: iconst_0
21: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
26: checkcast #7 // class java/lang/Integer
29: astore_2
30: return
LineNumberTable:
line 7: 0
line 8: 8
line 9: 19
line 10: 30
}
看到第18行代码就是将Object类型的对象强制转换为Integer的指令。我们完全可以将上面的代码转换为下面的代码,它所实现的效果跟上面的泛型是一模一样的。既然泛型也需要进行强制转换,所以泛型并不会提供运行时效率,不过可以大大降低编程时的出错概率。
public static void main(String[] args) {
List a = new ArrayList();
a.add(1);
Integer ai = (Integer)a.get(0);
}