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 erasurepublic 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 superclasspair.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 DateIntervalObject 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
``` 或者也可以标注整个方法。
