Java 语言的泛型实现方式是擦拭法(Type Erasure)。虚拟机没有泛型类型对象————所有对象都属于普通类。

类型擦除

无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型(无限定的变量用 Object)。
例如,Pair的原始类型如下所示:

  1. public class Pair {
  2. private Object first;
  3. private Object second;
  4. public Pair() { first = null; second = null; }
  5. public Pair(Object first, Object second) { this.first = first; this.second = second; }
  6. public Object getFirst() { return first; }
  7. public Object getSecond() { return second; }
  8. public void setFirst(Object newValue) { first = newValue; }
  9. }

因为 T 是一个无限定的变量,所以直接用 Object 替换。
原始类型用第一个限定的类型变量来替换,如果没有给定限定就用 Object 替换。

这也是为什么上一节中,如果有类,第一个必须是类的原因。

例如,类 Pair 中的类型变量没有显式的限定,因此,原始类型用 Object 替换 T。假定声明了一个不同的类型:

  1. public class Interval<T extends Comparable & Serializable> implements Serializable {
  2. private T lower;
  3. private T upper;
  4. public Interval(T first, T second) {
  5. if (first.compareTo(second) <= 0 ) { lower = first; upper = second; }
  6. else { lower = second; upper = first; }
  7. }
  8. }

原始类型 Interval 如下所示:

  1. public class Interval implements Serializable {
  2. private Comparable lower;
  3. private Comparable upper;
  4. public Interval(Comparable first, Comparable second) {...}
  5. }

如果这里切换限定为这样:public class Interval<T extends Serializable & Comparable> implements Serializable { 也就是 Serializable 在前,原始类型会用 Serializable 替换 T,但是编译器会在必要的时候使用强制转换,如 first.compareTo(second) 会变成:((Comparable)first).compareTo(second)。为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界列表的末尾。

翻译泛型表达式

当程序调用泛型的方法时,如果擦除返回类型,编译器插入强制类型转换。例如,下面这个语句序列:

  1. Pair<Employee> buddies = ...;
  2. Employee buddy = buddies.getFirst();

擦除 getFirst 的返回类型后将返回 Object 类型。编译器自动插入 Employee 的强制类型转换:

  1. Pair buddies = ...;
  2. Employee buddy = (Employee)buddies.getFirst();

翻译泛型方法

类型擦除也会出现在泛型方法中。程序员通常认为下述的泛型方法:

  1. public static <T extends Comparable> T min(T[] a)

是一个完整的方法族,而擦除类型之后,只剩下一个方法:

  1. public static Comparable min(Comparable[] a)

但是这样会造成多态问题。例如,有这样一个例子:

  1. public class DateInterval extends Pair<LocalDate> {
  2. public void setSecond(LocalDate second) {...}
  3. }

类擦除后变成:

  1. public class DateInterval extends Pair { // after erasure
  2. public void setSecond(LocalDate second) {...}
  3. }

上面的类中,存在另一个从 Pair 继承的 setSecond() ,即:

  1. public void setSecond(Object second)

这显然是一个不同的方法,因为它有一个不同类型的参数 Object,而不是 LocalDate。
如果有下列语句:

  1. DateInterval interval = new DateInterval(...);
  2. LocalDate aDate = LocalDate.of(...);
  3. Pair<LocalDate> pair = interval; // OK -- assignment to superclass
  4. pair.setSecond(aDate);

因为是继承的泛型类 Pair ,并且 setSecond(LocalDate) 中的参数也是同样的类,故根据上述的语句,是想让 pair.setSecond(aDate) 调用 DateInterval 中的 setSecond(LocalDate) 从而具有多态性。但是由于类型擦除导致 Pair 类中的 steSecond(Object) 参数为 Object ,从而与多态发生了冲突。
Java 使用桥方法(bridge method)来解决这个问题,编译器在 DateInterval 类中生成一个桥方法:

  1. public void setSecond(Object second) {
  2. setSecond( (LocalDate) second );
  3. }

这样就很好的解决了多态的问题。

注意:桥方法是编译器自动生成的。

但是,这样又会产生一些不好懂的点,假设 DateInterval 也覆盖了 getSecond()

  1. public class DateInterval extends Pair<LocalDate> {
  2. public LocalDate getSecond() { return (LocalDate) super.getSecond(); }
  3. }

在 DateInterval 类中,有两个 getSecond()

  1. LocalDate getSecond(); // defined in DateInterval
  2. Object getSecond(); // overrides the method defined in Pair to call the first method

正常是不能这样编写代码的,因为具有相同参数类型的两个方法是不合法的。但是在虚拟机中,用参数类型和返回类型确定一个方法。因此,编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。
Java 泛型转换的事实:

  • 虚拟机中没有泛型,只有普通的类和方法。
  • 所有的类型参数都用它们的限定类型替换。
  • 桥方法被合成来保持多态。
  • 为保持类型安全性,必要时插入强制类型转换。

    调用遗留代码

    JSlider 类中有个方法: ``` void setLabelTable(Dictionary table)
  1. 实现JSlider类时Java中还不存在泛型,所以这里的 Dictionary 是原始类型。不过,填充字典时,要使用泛型类型。

Dictionary lableTable = new Hashtable<>(); lableTable.put(0, new JLabel(new ImageIcon(“nine.gif”))); lableTable.put(20, new JLabel(new ImageIcon(“ten.gif”)));

  1. `Dictionary<Integer,Component>` 对象传递给 setLabelTable 时,编译器会发出一个警告。

slider.setLabelTable(labelTable); // Warning

  1. 同样,由一个遗留的类得到一个原始类型的对象。将它赋给一个参数化的类型变量,这样做会看到一个警告。例如:

Dictionary labelTable = slider.getLabelTable(); // Warning

  1. 在查看了警告之后,可以利用注解(annotation)使之消失。注释必须放在生成这个警告的代码所在的方法之前,如下:

@SuppressWarnings(“unchecked”) Dictionary labelTable = slider.getLabelTable(); // NO Warning

``` 或者也可以标注整个方法。