泛型的类型擦除
本文主要介绍泛型在虚拟机中的实现——使用类型擦除的方法。
类型擦除
Java虚拟机中没有泛型类型对象,所有的类都属于普通类。因此Java编译器使用一个普通的类来实现泛型,这个普通类称为相应泛型的原始类型。例如对于如下泛型程序:
public class Pair<T> {
private T first;
private T second;
public Pair() {
first = null;
second = null;
}
public Pair(T first T second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
public void setFirst(T newValue) {
first = newValue;
}
public void setSecond(T newValue) {
second = newValue;
}
}
它的原始类型如下:
public class Pair {
private Object first;
private Object second;
public Pair() {
first = null;
second = null;
}
public Pair(Object first Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
public void setFirst(Object newValue) {
first = newValue;
}
public void setSecond(Object newValue) {
second = newValue;
}
}
可以看到,<T>
没有了,类型变量T
被擦除了,并替换为了Object
。
这里的T
没有任何限定,因此替换为Object
。如果这里的T
有限定的话,会替换为其限定类型。
类型变量的限定使用如下形式:
<T extends BoundingType>
这里的BoundingType
就是限定类型。上面这种写法将T
限定为BoundingType
的子类型。
T
和BoundingType
可以是类,也可以是接口。因为Java中可以同时实现多个接口,所以这里的限定类型也可以有多个。多个限定类型使用如下形式:
<T extends BoundingType1 & BoundingType2 & BoundingType3 ...>
但是其中最多有一个可以是类,其他的只能是接口。
有多个限定类型时,会使用第一个限定类型进行类型擦除。
例如对于如下例子:
public class Interval<T extends Comparable & Serializable> implements Serializable {
private T lower;
private T upper;
...
public Interval(T first, T second) {
if(first.compareTo(second)) <= 0 {
lower = first;
upper = second;
}
else {
lower = second;
upper = first;
}
}
}
它的原始类型如下:
public class Interval implements Serializable {
private Comparable lower;
private Comparable upper;
...
public Interval(Comparable first, Comparable second) {
if(first.compareTo(second)) <= 0 {
lower = first;
upper = second;
}
else {
lower = second;
upper = first;
}
}
}
编译器的作用
尽管在虚拟机中,泛型类型实际上是一个普通的类,但是编译器不会让你像使用一个普通的类一样使用它。它会对你的使用加以限制,并自动地帮我们做一些事情,让泛型看起来像一个“泛型”。
这主要有以下两点:
- 调用一个泛型方法时,如果擦除了返回类型,编译器会插入强制类型转换。
- 编译器使用桥方法来保证泛型子类的多态性。
关于第一点,对于下面这个语句序列:
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
我们知道buddies.getFirst()
类型擦除之后返回类型是Object
,跟buddy
的类型是不一样的。这时候,编译器会自动插入到Employee
的强制类型转换。
对buddies.getFirst()
方法的调用实际上被编译器转换为两条虚拟机指令:
- 对原始方法
Pair.getFirst
的调用。 - 将返回的
Object
类型强制转换为Employee
类型。
关于第二点,对于下面这个类:
class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
if(second.compareTo(getFirst()) >= 0)
super.setSecond();
}
...
}
这里它对于setSecond
方法的重写应该是有效的,但是它类型擦除之后会变成:
class DateInterval extends Pair {
public void setSecond(LocalDate second) {...}
...
}
Pair
中setSecond
方法参数是Object
类型的,因此上面实际上是在重载方法,而不是在重写,DateInterval
中还会有一个从Pair
继承的方法:
public void setSecond(Object second)
两个方法是不同的,但是它们不应该是不同的。
这时候,编译器会自动生成一个桥方法:
public void setSecond(Object second) {
setSecond((LocalDate) second);
}
可以看到,编译器用我们编写的方法重写了原来的方法,从而使得我们看上去自己“重写了”原来的方法。
参考资料
书籍
Java核心技术 卷Ⅰ 基础知识(原书第11版) 第8章