铁文整理
12.5 泛型代码和虚拟机
虚拟机没有泛型类型对象——所有对象都属于普通类。在泛型实现的早期版本中,甚至能够将使用泛型的程序编译为在1.0虚拟机上运行的类文件!这个向后兼容性在Java泛型开发的后期被放弃了。如果使用Sub的编译器来编译使用_Java泛型的代码,结果类文件将不能在5.0之前的虚拟机上运行。
注释:如果想拥有泛型的优势,同时又保留旧的虚拟机字节码兼容性,请查阅:http://sourceforge.net/projects/retroweaver。Retroweaver程序重写类文件以便与旧的虚拟机兼容。
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object)。
例如,Pair<T>的原始类型如下所示:
public class Pair {
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;
}
private Object first;
private Object second;
}
因为T是一个无限定的变量,所以直接用Object替换。
结果是一个普通的类,就好像泛型引入Java语言之前已经实现的那样。
在程序中可以包含不同类型的Pair,例如,Pair<String>或Pair<GregorianCalendar>。而擦除类型后就变成原始的Pair类型了。
C++注释:就这点而言,Java泛型与C++模板有很大的区别。C++中每个糢板的实例化产生不同的类型,这一现象称为“糢板代码膨胀。Java不存在这个问题的困扰。
原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换。例如,类Pair<T>中的类型变量没有显式的限定,因此,原始类型用Object替换T。假定声明了一个不同的类型。
public class Interval<T extends Comparable & Serializable> implements Serializable {
public Interval(T first, T second) {
if (first.compareTo(second) <= 0) {
lower = first;
upper = second;
} else {
lower = second;
upper = first;
}
}
private T lower;
private T upper;
}
原始类型Interval如下所示:
public class Interval implements Serializable {
public Interval(Comparable first, Comparable second) {
...
}
private Comparable lower;
private Comparable upper;
}
注释:读者可能想要知道切换限定:class Interval<Comparable& Serializable>会发生什么。如果这样做,原始类型用Serializable替换T,而编译器在必要时要向Comparable插入强制类型转换。为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界列表的末尾。
12.5.1 翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。例如,下面这个语句序列
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
擦除getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换。也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
-
对原始方法Pair.getFirst的调用。
-
将返回的Object类型强制转换为Employee类型。
当存取一个泛型域时也要插入强制类型转换。假设Pair类的first域和second域都是公有的(也许这不是一种好的编程风格,但在Java中是合法的)。表达式Employee buddy = buddies.first;也会在Java中插入强制类型转换。
12.5.2 翻译泛型方法
类型擦除也会出现在泛型方法中。程序员通常认为下述的泛型方法
public static <T extends Comparable> T min(T[] a)
是一个完整的方法族,而擦除类型之后,只剩下一个方法:
public static Comparable min(Comparable [] a)
注意,类型参数T已经被擦除了,只留下了限定类型Comparable。
方法的擦除带来了两个复杂问题。看—看下面这个示例:
class DateInterval extends Pair<Date> {
public void setSecond(Date second) {
if (second.compareTo(getFirst()) >= 0)
super.setSecond(second);
}
}
一个日期区间是一对Date对象,并且需要覆盖这个方法来确保第二个值永远不小于第一个值。这个类檫除后变成
class DateInterval extends Pair {
public void setSecond(Date second) {
……
}
}
令人感到奇怪的是,存在另一个从Pair继承的setSecond方法,即
public void setSecond(Object second)
这显然是一个不同的方法,因为它有一个不同类型的参数——Object,而不是Date。然而,不应该不一样。考虑下面的语句序列:
DateInterval interval = new DateInterval();
Pair<Date> pair = interval; // OK—assignment to superclass
pair.setSecond(aDate);
这里,希望对setSecond的调用具有多态性,并调用最合适的那个方法。由于pair引用DateInterval对象,所以应该调用DateInterval.setSecond。问题在于类型擦除与多态发生了冲突。要解决这个问题,就需要编译器在DateInterval类中生成一个桥方法(bridge method):
public void setSecond(Object second) {
setSecond((Date) second);
}
要想了解它的工作过程,请仔细地跟踪下列语句的执行:
pair.setSecond(aDate);
变量pair已经声明为类型Pair<Date>,并且这个类型只有一个简单的方法叫setSecond,即setSecond(Object)。虚拟机用pair引用的对象调用这个方法。这个对象是DateInterval类型的,因而将会调用DateInterval.setSecond(Object)方法。这个方法是合成的桥方法。它调用DateInterval.setSecond(Date),这正是我们所期望的操作效果。
桥方法可能会变得十分奇怪。假设DateInterval方法也覆盖了getSecond方法:
class DateInterval extends Pair<Date> {
public Date getSecond() {
return (Date) super.getSecond();
}
}
在擦除的类型中,有两个getSecond方法:
Date getSecond() // defined in DateInterval
Object getSecond() // defined in Pair
不能这样编写Java代码(在这里,具有相同参数类型的两个方法是不合法的)。它们都没有参数。但是,在虚拟机中,用参数类型和返回类型确定一个方法。因此,编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。
注释:桥方法不仅用于泛型类型,第5章已经讲过,从Java SE 5.0开始,在一个方法覆盖另一个方法时可以指定一个更严格的返回类型。例如,
public class Employee implements Cloneable {
public Employee clone() throws CloneNotSupportedException { ... }
}
Object.clone和Employee.clone方法被说成具有协变的返回类型(covariant return types)。实际上,Employee类有两个克隆方法:
Employee clone() // defined above
Object clone() // synthesized bridge method, overrides Object.clone
合成的桥方法调用了新定义的方法。
总之,需要记住有关Java泛型转换的事实:
-
虚拟机中没有泛型,只有普通的类和方法。
-
所有的类型参数都用它们的限定类型替换。
-
桥方法被合成来保持多态。
-
为保持类型安全性,必要时插入强制类型转换。
12.5.3 调用遗留代码
许多Java代码编写于Java SE 5.0之前。如果泛型类不能与这些代码互操作,就得不到广泛的应用。幸运的是,可以直接将泛型类与遗留的API中原始的对应概念放在—起使用。
下面看一个具体的示例。要想设置一个JSlider标签,可以使用方法:
void setLabelTable(Dictionary table)
在第九章中,使用下述代码填充标签表:
Dictionary<Integer, Component> labelTable = new Hashtable<Integer, Component>();
labelTable.put(0, new JLabel("A"));
labelTable.put(20, new JLabel("B"));
……
slider.setLabelTable(labelTable);
在Java SE 5.0中,Dictionary和Hashtable类被转换成泛型类。因此,可以用Dictionary<Integer, Component>取代原始的Dictionary。但是,当将Dictionary<Integer, Component>对象传递给setLabelTable时,编译器会发出一个警告。
Dictionary<Integer, Component> labelTable = ...;
slider.setLabelTable(labelTable); // WARNING
毕竟,编译器无法确定setLabelTable可能会对Dictionary对象做什么操作。这个方法可能会用字符串替换所有的关键字,这就打破了关键字类型为整型的承诺,未来的操作有可能会产生强制类型转换的异常。
这个警告对操作不会产生什么影响,最多考虑一下JSlider有可能用Dictionary对象做什么就可以了。在这里十分清楚,JSIider只阅读这个信息,因此可以忽略这个警告。
现在,看一个相反的情形,由一个遗留的类得到一个原始类型的对象。可以将它赋给一个参数化的类型变量,当然,这样做会看到一个警告。例如,
Dictionary<Integer, Component> labelTable = slider.getLabelTable(labelTable); // WARNING
这就行了。再看一看警告,确保标签表已经包含了Integer和Component对象,当然,从来也不会有绝对的承诺。恶意的编码者可能会在滑块中设置不同的Dictionary。然而,这种情况并不会比Java SE 5.0之前的情况更槽。最差的情况就是程序抛出一个异常。
在査看了警告之后,可以利用注释(annotation)使之消失。注释必须放在生成这个警告的代码所在的方法之前,如下:
@SuppressWarnings("unchecked")
public void configureSlider() { ... }
遗憾的是,这个注释也关闭了对方法内部代码的栓査。将潜在不安全的代码分隔在一些不同的方法中是一个不错主意,这样可以更加容易地进行査看。
注释:Hashtable类是抽象类Dictionary的具体子类。自从Dictionary和Hashtable类被Java SE 1.2的Map接口和HashMap类取代以来,它们都被标记为“废弃”。尽管从表面上看,它们仍然是有效的。毕竟,JSlider类是在Java SE 1.3中增加的。那时程序员不知道Map类吗?这会给在不远的未来程序员接受泛型带来希望吗?这就是与遗留代码保持一致的方式。