分享

擦除实现的java泛型

 且看且珍惜 2014-12-05

Java中的泛型不是语言内在的机制,而是后来添加的特性,这样就带来一个问题:非泛型代码和泛型代码的兼容性。泛型是JDK1.5才添加到Java中的,那么之前的代码全部都是非泛型的,它们如何运行在JDK1.5及以后的VM上?为了实现这种兼容性,Java泛型被局限在一个很狭窄的地方,同时也让它变得难以理解,甚至可以说是Java语言中最难理解的语法。

擦除

为了实现与非泛型代码的兼容,Java语言的泛型采用擦除(Erasure)来实现,也就是泛型基本上由编译器来实现,由编译器执行类型检查和类型推断,然后在生成字节码之前将其清除掉,虚拟机是不知道泛型存在的。这样的话,泛型和非泛型的代码就可以混合运行,当然了,也显得相当混乱。

在使用泛型时,会有一个对应的类型叫做原生类型(raw type),泛型类型会被擦除到原生类型,如Generic<T>会被查处到Generic,List<String>会被查处到List,由于擦除,在虚拟机中无法获得任何类型信息,虚拟机只知道原生类型。下面的代码将展示Java泛型的真相-擦除

  1. class Erasure<T> {  
  2.     private T t;  
  3.       
  4.     public void set(T t) {  
  5.         this.t = t;  
  6.     }  
  7.       
  8.     public T get() {  
  9.         return t;  
  10.     }  
  11.       
  12.     public static void main(String[] args) {      
  13.         Erasure<String> eras = new Erasure<String>();  
  14.         eras.set("not real class type");  
  15.         String value = eras.get();  
  16.           
  17.     }  
  18. }  
使用javap反编译class文件,得到如下代码:
  1. class com.think.generics.Erasure<T> {  
  2.   com.think.generics.Erasure();  
  3.     Code:  
  4.        0: aload_0         
  5.        1: invokespecial #12                 // Method java/lang/Object."<init>":()V  
  6.        4return          
  7.   
  8.   public void set(T);  
  9.     Code:  
  10.        0: aload_0         
  11.        1: aload_1         
  12.        2: putfield      #23                 // Field t:Ljava/lang/Object;  
  13.        5return          
  14.   
  15.   public T get();  
  16.     Code:  
  17.        0: aload_0         
  18.        1: getfield      #23                 // Field t:Ljava/lang/Object;  
  19.        4: areturn         
  20.   
  21.   public static void main(java.lang.String[]);  
  22.     Code:  
  23.        0new           #1                  // class com/think/generics/Erasure  
  24.        3: dup             
  25.        4: invokespecial #30                 // Method "<init>":()V  
  26.        7: astore_1        
  27.        8: aload_1         
  28.        9: ldc           #31                 // String not real class type  
  29.       11: invokevirtual #33                 // Method set:(Ljava/lang/Object;)V  
  30.       14: aload_1         
  31.       15: invokevirtual #35                 // Method get:()Ljava/lang/Object;  
  32.       18: checkcast     #37                 // class java/lang/String  
  33.       21: astore_2        
  34.       22return          
  35. }  
从反编译出来的字节码可以看到,泛型Erasure<T>被擦除到了Erasure,其内部的字段T被擦除到了Object,可以看到get和set方法中都是把t作为Object来使用的。最值得关注的是,反编译代码的倒数第三行,对应到Java代码就是String value = eras.get();编译器执行了类型转换。这就是Java泛型的本质:对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。这样的泛型真的是泛型吗?

即便我们可以说,Java中的泛型确实不是真正的泛型,但是它带来的好处还是显而易见的,它使得Java的类型安全前进了一大步,原本需要程序员显式控制的类型转换,现在改由编译器来实现,只要你按照泛型的规范去编写代码,总会得到安全的保障。在这里,我们不得不思考一个问题,理解Java泛型,那么其核心目的是什么?我个人认为,Java泛型的核心目的在于安全性,尤其是在理解泛型通配符时,一切奇怪的规则,归根结底都是处于安全的目的。

类型信息的丢失

由于擦除的原因,在泛型代码内部,无法获得任何有关泛型参数类型的信息。在运行时,虚拟机无法获得确切的类型信息,一切以来确切类型信息的工作都无法完成,比如instanceof操作,和new表达式,

  1. class  Erasure<T>  {  
  2.     public void f() {  
  3.         if(arg instanceof T) //Error  
  4.         T ins = new T();//Error  
  5.         T[] array = new T[10];//error  
  6.     }  
  7. }  
那么在需要具体的类型信息时,我们就要记住Class对象来实现了,凡是在运行时需要类型信息的地方,都使用Class对象来进行操作,比如:
  1. class Erasure<T> {  
  2.     private Class<T> clazz;  
  3.     Erasure(Class<T> kind) {  
  4.         clazz = kind;  
  5.     }  
  6.     public void f() {  
  7.         if(clazz.isInstance(arg)) {}  
  8.         T t = clazz.newInstance();//必须要有无参构造方法  
  9.     }  
  10. }  

泛型类中的数组

数组是Java语言中的内建特性,将泛型与数组结合就会有一些难以理解的问题。首先Java中的数组是协变的,Integer是Number的子类,所以Integer[]也是Number[]的子类,凡是使用Number[]的地方,都可以使用Integer[]来代替,而泛型是不协变的,比如List<String>不是List<Object>的子类,在通配符中,会详细讨论这些情况。

由于无法获得确切的类型信息,我们怎么样创建泛型数组呢?在Java中,所有类的父类都是Object,所以可以创造Object类型的数组来代替泛型数组:

  1. public class Array<T> {  
  2.     private int size = 0;  
  3.     private Object[] array;  
  4.       
  5.     public Array(int size) {  
  6.         this.size = size;  
  7.         array = new Object[size];  
  8.     }  
  9.     //编译器会保证插入进来的是正确类型  
  10.     public void put(int index,T item) {  
  11.         array[index] = item;  
  12.     }  
  13.       
  14.     //显式的类型转换  
  15.     public T get(int index) {  
  16.         return (T)array[index];  
  17.     }  
  18.       
  19.     public T[] rep() {  
  20.         return (T[])array;  
  21.     }  
  22.       
  23.     private static class Father {}  
  24.     private static class Son extends Father {}  
  25.       
  26.     public static void main(String[] args) {  
  27.         Array<String> instance = new Array<String>(10);  
  28.         String[] array = instance.rep();//异常  
  29.           
  30.     }  
  31. }  
在上面的代码中,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:
  1. String[] array = new String[10];  
  2. 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对象和数组的长度:

  1. private Class<T> kind;  
  2.     public ArrayMaker(Class<T> kind ) {  
  3.         this.kind = kind;  
  4.     }  
  5.       
  6.     @SuppressWarnings("unchecked")  
  7.     T[] create(int size) {  
  8.         T[] array = (T[])Array.newInstance(kind, size);  
  9.         System.out.println(array.getClass().getName());  
  10.         return array;  
  11.     }  
这样构造的就是具体类型的数组,比如传递进来的是String.class,那么调用create方法会打印:[Ljava.lang.String,在底层构造的确实是String类型的数组。使用这样的方式创建数组,应该是一种更优雅,更安全的方式。
以上内容介绍了Java泛型的实质,它的泛型更像是一颗语法糖,一颗由编译器包括的语法糖。由编译器实现的泛型又有诸多奇怪的限制,可泛型的功能又是如此强大,使用的又是如此频繁,所以对泛型的抱怨一直在持续,同时,泛型又是个绕不过去的弯。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多