Java 语言的泛型实现方式是擦拭法(Type Erasure)。虚拟机没有泛型类型对象————所有对象都属于普通类。
类型擦除
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型(无限定的变量用 Object)。
例如,Pair的原始类型如下所示:
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; }
}
因为 T 是一个无限定的变量,所以直接用 Object 替换。
原始类型用第一个限定的类型变量来替换,如果没有给定限定就用 Object 替换。
这也是为什么上一节中,如果有类,第一个必须是类的原因。
例如,类 Pair
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; }
}
}
原始类型 Interval 如下所示:
public class Interval implements Serializable {
private Comparable lower;
private Comparable upper;
public Interval(Comparable first, Comparable second) {...}
}
如果这里切换限定为这样:
public class Interval<T extends Serializable & Comparable> implements Serializable {
也就是 Serializable 在前,原始类型会用 Serializable 替换 T,但是编译器会在必要的时候使用强制转换,如first.compareTo(second)
会变成:((Comparable)first).compareTo(second)
。为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界列表的末尾。
翻译泛型表达式
当程序调用泛型的方法时,如果擦除返回类型,编译器插入强制类型转换。例如,下面这个语句序列:
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
擦除 getFirst 的返回类型后将返回 Object 类型。编译器自动插入 Employee 的强制类型转换:
Pair buddies = ...;
Employee buddy = (Employee)buddies.getFirst();
翻译泛型方法
类型擦除也会出现在泛型方法中。程序员通常认为下述的泛型方法:
public static <T extends Comparable> T min(T[] a)
是一个完整的方法族,而擦除类型之后,只剩下一个方法:
public static Comparable min(Comparable[] a)
但是这样会造成多态问题。例如,有这样一个例子:
public class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {...}
}
类擦除后变成:
public class DateInterval extends Pair { // after erasure
public void setSecond(LocalDate second) {...}
}
上面的类中,存在另一个从 Pair 继承的 setSecond()
,即:
public void setSecond(Object second)
这显然是一个不同的方法,因为它有一个不同类型的参数 Object,而不是 LocalDate。
如果有下列语句:
DateInterval interval = new DateInterval(...);
LocalDate aDate = LocalDate.of(...);
Pair<LocalDate> pair = interval; // OK -- assignment to superclass
pair.setSecond(aDate);
因为是继承的泛型类 PairsetSecond(LocalDate)
中的参数也是同样的类,故根据上述的语句,是想让 pair.setSecond(aDate)
调用 DateInterval 中的 setSecond(LocalDate)
从而具有多态性。但是由于类型擦除导致 Pair 类中的 steSecond(Object)
参数为 Object ,从而与多态发生了冲突。
Java 使用桥方法(bridge method)来解决这个问题,编译器在 DateInterval 类中生成一个桥方法:
public void setSecond(Object second) {
setSecond( (LocalDate) second );
}
这样就很好的解决了多态的问题。
注意:桥方法是编译器自动生成的。
但是,这样又会产生一些不好懂的点,假设 DateInterval 也覆盖了 getSecond()
:
public class DateInterval extends Pair<LocalDate> {
public LocalDate getSecond() { return (LocalDate) super.getSecond(); }
}
在 DateInterval 类中,有两个 getSecond()
:
LocalDate getSecond(); // defined in DateInterval
Object getSecond(); // overrides the method defined in Pair to call the first method
正常是不能这样编写代码的,因为具有相同参数类型的两个方法是不合法的。但是在虚拟机中,用参数类型和返回类型确定一个方法。因此,编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。
Java 泛型转换的事实:
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都用它们的限定类型替换。
- 桥方法被合成来保持多态。
- 为保持类型安全性,必要时插入强制类型转换。
调用遗留代码
JSlider 类中有个方法: ``` void setLabelTable(Dictionary table)
实现JSlider类时Java中还不存在泛型,所以这里的 Dictionary 是原始类型。不过,填充字典时,要使用泛型类型。
Dictionary
将 `Dictionary<Integer,Component>` 对象传递给 setLabelTable 时,编译器会发出一个警告。
slider.setLabelTable(labelTable); // Warning
同样,由一个遗留的类得到一个原始类型的对象。将它赋给一个参数化的类型变量,这样做会看到一个警告。例如:
Dictionary
在查看了警告之后,可以利用注解(annotation)使之消失。注释必须放在生成这个警告的代码所在的方法之前,如下:
@SuppressWarnings(“unchecked”)
Dictionary
``` 或者也可以标注整个方法。