泛型-擦除实现的Java泛型

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泛型的实质,它的泛型更像是一颗语法糖,一颗由编译器包括的语法糖。由编译器实现的泛型又有诸多奇怪的限制,可泛型的功能又是如此强大,使用的又是如此频繁,所以对泛型的抱怨一直在持续,同时,泛型又是个绕不过去的弯。

转载请注明:喻红叶《泛型-擦除实现的Java泛型》

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值