为什么Java的泛型要使用类型擦除?
从技术来说,Java完全可以不使用类型擦除而直接实现”真泛型”。然而,Java诞生的时候,是没有包括泛型的,10年之后,Java才想实现类似于C++模板的概念,即泛型。由于Java类库是非常宝贵的资源,因此必须保证向后兼容。如果要实现“真泛型”,不仅需要修改 JVM 的源代码,让 JVM 能正确读取和校验泛型信息;而且为了兼容,需要为原本不支持泛型的 API 都添加相应的泛型 API。这样的修改工作量是无法想象的。因此Java设计者采取了 “类型擦除” 这种机制作为折中的实现方式。
什么是类型擦除?
在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉,这个过程就称为类型擦除。
我们以一个例子来验证Java的类型擦除机制。对于下面的代码,最后输出的结果为true。
ArrayList<Integer> intergerList = new ArrayList<Integer>();
ArrayList<String> stringList = new ArrayList<String>();
System.out.println(intergerList.getClass() == stringList.getClass()); // true
我们定义的两个列表,一个是String
泛型,一个是Integer
泛型,但是它们的getClass()
方法得到的类信息居然是一样的。这说明泛型类型String
和Integer
在运行之前就被擦除掉了,在运行时无法知道定义时的具体类型。
在类型擦除后的实际类型是什么呢?根据上面所说的向后兼容,擦除后的实际类型应该能被之前没有泛型时的Java程序接受。因此Java将具体的类型擦除为原始类型。原始类型就是擦除泛型信息,最后在字节码中的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供。类型变量擦除时,使用其限定类型替换(无限定时用Object替换)。
举例来说,如果我们定义下面这样一个带泛型的类:
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() {
return data;
}
// ...
}
那类型擦除后,它的实际定义应该类似于:
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() {
return data;
}
// ...
}
所有的泛型T都被替换为Object。
类型擦除带来的问题
1.运行时的类型检测问题:由于类型变量会在运行前被擦除掉,那如何检测到向ArrayList<String>中添加非String类型的对象这种类型错误呢?
解决方法:利用编译器进行静态检查。编译器会先检查代码中的泛型类型,如果存在类型不匹配,会直接报错,只有在类型匹配时才能进行后续的类型擦除。比如:
List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; // compiler error
myNums.add(3.14);
static long sum(List<Number> numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
List<Integer> myInts = Arrays.asList(1,2,3,4,5);
List<Long> myLongs = Arrays.asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0);
sum(myInts);
sum(myLongs); // compiler error
sum(myDoubles);
2.返回泛型类型的函数怎么办?所有的泛型类型都会被替换为原始类型,那返回泛型类型的函数实际上返回的是原始类型的对象,难道还需要手动的类型转化吗?
解决方法:自动生成类型转换。ArrayList
的get方法源代码如下:
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
在return
之前,会根据泛型的类型进行强转。即使泛型信息会被擦除,但是会将(E) elementData[index]
编译为(Date) elementData[index]
。因此不需要自己手动进行强转,在类型擦除时会自动地在结果字节码中插入强制类型转换。
3.不能使用泛型数组。如果我们假设在 Java 中可以创建泛型数组,那么看如下代码:
List<String>[] s = new ArrayList<String>[1];
List<Integer> i = Arrays.asList(1);
Object[] o = s;
o[0] = i
Java的类型系统规定,子类数组是父类数组的子类,因此Object[] o = s;是正确的。如果我们假设可以创建泛型数组,那上述代码没有任何问题。但是最后一行代码将 List<Integer> 类型的对象赋给了List<String>类型的数组,这个问题在编译时无法发现,只能在运行时出现问题。然而,类型擦除机制要求编译器能够检测所有类型不匹配的问题。因此Java中禁止创建泛型数组,这样编译器就可以检测所有的类型不匹配问题。