泛型代码和虚拟机
虚拟机没有泛型类型对象(所有对象都属于普通类)
类型擦除
无论何时定义一个泛型代码,都会自动提供一个相应的原始类型。
这个原始类型的名字就是去掉类型参数之后的泛型类型名。
类型变量会被擦除,并替换为其限定类型(或者,对于无限定的变量则替换为Object)。
例如,Pair
// 因为T是一个无限定的变量,所以直接用Object替换
// 结果是一个普通的类,就像Java在引入泛型之前实现的类一样
public class Pair{
private Object first;
private Object second;
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;
}
}
程序中可以包含不同类型的Pair,例如,pari
不过擦除类型之后,它们都会变成原始的Pair类型。
原始类型用第一个限定来替换类型变量,如果没有给定限定,就替换成Object。
public class Interval<T extends Comparable & Serializable> implements Serializable{
private T lower;
private T upper;
public Interval(T first, T second){
...
}
}
// 原始类型的Interval如下所示:
public class Interval implements Serializable{
private Comparable lower;
private Comparable upper;
public Interval(Comparable first, Comparable second){...}
}
// 如果切换为class Interval<T extends Serializable & Comparable>
// 那么,原始类型会用Serializable替换T,而编译器会在必要时要向Comparable插入强制类型转换。
// 为提高效率,应该将标签接口(即没有方法的接口)放在限定列表的末尾
转换泛型表达式
编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。
例如
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
getFirst擦除类型之后是Object。编译器自动插入转换到Employee的强制类型转换。
也就是说,编译器把这个方法调用转换为两条虚拟机指令:
- 对原始方法Pair.getFirst的调用。
- 将返回的Object类型强制转换为Employee类型。
当访问一个泛型字段时也要插入强制类型转换。
假设Pair类的first字段和second字段都是公共的。
表达式Employee buddy = buddies.first;
也会在结果字节码中插入强制类型转换。
转换泛型方法
类型擦除也会出现在泛型方法中。
通常认为类似这样的泛型方法public static <T extends Comparable>
是整个一组方法,
而擦除类型之后,只剩下一个方法:public static Comparable min(Comparable[] a)
注意,类型参数T已经被擦除了,只留下了限定类型Comparable。
方法的擦除带来了两个复杂问题。
如下示例:
class DateInterval extends Pair<LocalDate>{
public void setSecond(LocalDate second){...}
}
// 这个类擦除之后变成
class DateInterval extends Pair{
public void setSecond(LocalDate second){...}
public void setSecond(Object second){...} // 这显然是一个不同的方法,因为它有一个不同类型的参数
}
// 参考如下语句序列
var interval = new DateInterval{...}
Pair<LocalDate> pair = interval;
pair.setSecond(aDate);
我们希望setSecond调用具有多态性,会调用最适合的方法。
由于pair引用了一个DateInterval对象。所以应该调用DateInterval.setSecond.
但是,类型擦除与多态发生了冲突。
为了解决这个问题,编译器在DateInterval类中生成了一个桥方法:public void setSecond(Object second){...}
因为变量pair已经声明为类型Pair
虚拟机在pair引用的对象上调用这个方法。这个对象是DateInterval类型,所以将会调用DateInterval.setSecond(Object)方法。这个方法是合成的桥方法。
它会调用DateInterval.setSecond(LocalDate),这正是我们想要的。
桥方法可能会变得很奇怪,假如DateInterval类也覆盖了getSecond方法:
class DateInterval extends Pair<LocalDate>{
public LocalDate getSecond(){...}
}
// 在DateInterval类中,有2个get方法
LocalDate getSecond(){...}
Object getSecond(){...}
// 不能这样编写Java代码(两个方法有相同的参数类型是不合法的,在这里,两个方法都没有参数)
// 但是,在虚拟机中,会由参数类型和返回类型共同指定一个方法。
// 因此,编译器可以为两个仅返回类型不同的方法生成字节码,虚拟机能够正确处理这种情况。
桥方法不仅仅用于泛型类型。
一个方法覆盖另一个方法时,可以指定一个更严格的返回类型,这是合法的。
Java泛型转换总结
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的参数类型都会替换为它们的限定类型
- 会合成桥方法来保持多态
- 为保持类型安全性,必要时会插入强制类型转换