Java中的泛型不是语言内在的机制,而是后来添加的特性,这样就带来一个问题:非泛型代码和泛型代码的兼容性。泛型是JDK1.5才添加到Java中的,那么之前的代码全部都是非泛型的,它们如何运行在JDK1.5及以后的VM上?为了实现这种兼容性,Java泛型被局限在一个很狭窄的地方,同时也让它变得难以理解,甚至可以说是Java语言中最难理解的语法。
擦除
为了实现与非泛型代码的兼容,Java语言的泛型采用擦除(Erasure)来实现,也就是泛型基本上由编译器来实现,由编译器执行类型检查和类型推断,然后在生成字节码之前将其清除掉,虚拟机是不知道泛型存在的。这样的话,泛型和非泛型的代码就可以混合运行,当然了,也显得相当混乱。
在使用泛型时,会有一个对应的类型叫做原生类型(raw type),泛型类型会被擦除到原生类型,如Generic<T>会被查处到Generic,List<String>会被查处到List,由于擦除,在虚拟机中无法获得任何类型信息,虚拟机只知道原生类型。下面的代码将展示Java泛型的真相-擦除:
- class Erasure<T> {
- private T t;
-
- public void set(T t) {
- this.t = t;
- }
-
- public T get() {
- return t;
- }
-
- public static void main(String[] args) {
- Erasure<String> eras = new Erasure<String>();
- eras.set("not real class type");
- String value = eras.get();
-
- }
- }
使用javap反编译class文件,得到如下代码:
- class com.think.generics.Erasure<T> {
- com.think.generics.Erasure();
- Code:
- 0: aload_0
- 1: invokespecial #12 // Method java/lang/Object."<init>":()V
- 4: return
-
- public void set(T);
- Code:
- 0: aload_0
- 1: aload_1
- 2: putfield #23 // Field t:Ljava/lang/Object;
- 5: return
-
- public T get();
- Code:
- 0: aload_0
- 1: getfield #23 // Field t:Ljava/lang/Object;
- 4: areturn
-
- public static void main(java.lang.String[]);
- Code:
- 0: new #1 // class com/think/generics/Erasure
- 3: dup
- 4: invokespecial #30 // Method "<init>":()V
- 7: astore_1
- 8: aload_1
- 9: ldc #31 // String not real class type
- 11: invokevirtual #33 // Method set:(Ljava/lang/Object;)V
- 14: aload_1
- 15: invokevirtual #35 // Method get:()Ljava/lang/Object;
- 18: checkcast #37 // class java/lang/String
- 21: astore_2
- 22: return
- }
从反编译出来的字节码可以看到,泛型Erasure<T>被擦除到了Erasure,其内部的字段T被擦除到了Object,可以看到get和set方法中都是把t作为Object来使用的。最值得关注的是,反编译代码的倒数第三行,对应到Java代码就是String value = eras.get();编译器执行了类型转换。这就是Java泛型的本质:对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。这样的泛型真的是泛型吗?
即便我们可以说,Java中的泛型确实不是真正的泛型,但是它带来的好处还是显而易见的,它使得Java的类型安全前进了一大步,原本需要程序员显式控制的类型转换,现在改由编译器来实现,只要你按照泛型的规范去编写代码,总会得到安全的保障。在这里,我们不得不思考一个问题,理解Java泛型,那么其核心目的是什么?我个人认为,Java泛型的核心目的在于安全性,尤其是在理解泛型通配符时,一切奇怪的规则,归根结底都是处于安全的目的。
类型信息的丢失
由于擦除的原因,在泛型代码内部,无法获得任何有关泛型参数类型的信息。在运行时,虚拟机无法获得确切的类型信息,一切以来确切类型信息的工作都无法完成,比如instanceof操作,和new表达式,
- class Erasure<T> {
- public void f() {
- if(arg instanceof T) //Error
- T ins = new T();//Error
- T[] array = new T[10];//error
- }
- }
那么在需要具体的类型信息时,我们就要记住Class对象来实现了,凡是在运行时需要类型信息的地方,都使用Class对象来进行操作,比如:
- class Erasure<T> {
- private Class<T> clazz;
- Erasure(Class<T> kind) {
- clazz = kind;
- }
- public void f() {
- if(clazz.isInstance(arg)) {}
- T t = clazz.newInstance();//必须要有无参构造方法
- }
- }
泛型类中的数组
数组是Java语言中的内建特性,将泛型与数组结合就会有一些难以理解的问题。首先Java中的数组是协变的,Integer是Number的子类,所以Integer[]也是Number[]的子类,凡是使用Number[]的地方,都可以使用Integer[]来代替,而泛型是不协变的,比如List<String>不是List<Object>的子类,在通配符中,会详细讨论这些情况。
由于无法获得确切的类型信息,我们怎么样创建泛型数组呢?在Java中,所有类的父类都是Object,所以可以创造Object类型的数组来代替泛型数组:
- public class Array<T> {
- private int size = 0;
- private Object[] array;
-
- public Array(int size) {
- this.size = size;
- array = new Object[size];
- }
- //编译器会保证插入进来的是正确类型
- public void put(int index,T item) {
- array[index] = item;
- }
-
- //显式的类型转换
- public T get(int index) {
- return (T)array[index];
- }
-
- public T[] rep() {
- return (T[])array;
- }
-
- private static class Father {}
- private static class Son extends Father {}
-
- public static void main(String[] args) {
- Array<String> instance = new Array<String>(10);
- String[] array = instance.rep();//异常
-
- }
- }
在上面的代码中,get()和put()都可以正确的运行,编译器会保证类型的正确性。但是当rep()返回时赋给String[]类型的数组,则会抛出ClassCastException异常,抛出这样的异常是在意料之中的。在Java中,数组其实是一个对象,每一个类型的数组都后一个对应的类,这个类是虚拟机生成,比如上面的代码中,我们定义了Object数组,在运行时会生成一个名为"[Ljava.lang.Object"的类,它代表Object的一维数组;同样的,定义String[]数组,其对应的类是"[Ljava.lang.String"。从类名就可以看出,这些代表数组的类都不是合法的Java类名,而是由虚拟机生成,虚拟机在生成类是根据的是实际构造的数组类型,你构造的是Object类型的数组,它生成的就是代表Object类型的数组的类,无论你把它转型成什么类型。换句话说,没有任何方式可以推翻底层数组的类型。前面说到,数组是协变的,也就是说[Ljava.lang.Object其实是[Ljava.lang.String的父类,比如下面的代码会得到true:
- String[] array = new String[10];
- System.out.println(array instanceof Object[]);
所以在将rep()返回值赋给String[]类型时,它确实是发生了类型转换,只不过这个类型转换不是数组元素的转换,并不是把Object类型的元素转换成String,而是把[Ljava.lang.Object转换成了Ljava.lang.String,是父类对象转换成子类,必然要抛出异常。那么问题就出来了,我们使用泛型就是为了获得更加通用的类型,既然我声明的是Array<String>,往里存储的元素是String,得到的元素也是String,我理所应当的认为,我获得的数组应该也是String[],如果我这么做,你却给我抛异常,这是几个意思啊!
导致这个问题的罪魁还是擦除,由于擦除,没有办法这样这样定义数组:T[] array = new T[size];为了产生具体类型的数组,只能借助于Class对象,在Java类库提供的Array类提供了一个创造数组的方法,它需要数组元素类型的Class对象和数组的长度:
- private Class<T> kind;
- public ArrayMaker(Class<T> kind ) {
- this.kind = kind;
- }
-
- @SuppressWarnings("unchecked")
- T[] create(int size) {
- T[] array = (T[])Array.newInstance(kind, size);
- System.out.println(array.getClass().getName());
- return array;
- }
这样构造的就是具体类型的数组,比如传递进来的是String.class,那么调用create方法会打印:[Ljava.lang.String,在底层构造的确实是String类型的数组。使用这样的方式创建数组,应该是一种更优雅,更安全的方式。
以上内容介绍了Java泛型的实质,它的泛型更像是一颗语法糖,一颗由编译器包括的语法糖。由编译器实现的泛型又有诸多奇怪的限制,可泛型的功能又是如此强大,使用的又是如此频繁,所以对泛型的抱怨一直在持续,同时,泛型又是个绕不过去的弯。
|