12.5 泛型代码和虚拟机
虚拟机没有泛型类型对象--所有对象都属于普通类. 无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type).原始类型的名字就是删除类型参数后的泛型类型名.擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object).例如,Pair<T>的原始类型如下所示:
public class Pair
{
private Object first;
private Object second;
public Pair(Object first, Object second)
{
this.first = first;
this.second = second;
}
...
}
因为T是一个无限定的变量,所以直接用Object替换.
在程序中可以包含不同类型的Pair,例如,Pair<String>或Pair<GregorianCalendar> .而擦除类型后就变成原始的Pair类型了.
注释:就这点而言,Java泛型与C++模板有很大的区别.C++中每个模板的实例化产生不同的类型,这一现象称为"模板化膨胀",Java不存在这个问题的困扰.
原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换.例如,类Pair<T>中的类型变量没有显式的限定,因此,原始类型用Object替换T .假定声明了一个不同的类型.
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 = second;
}
}
}
原始类型Interval如下所示:
public class Interval implements Serializable
{
private Comparable lower;
private Comparable upper;
...
public Interval(Comparable first, Comparable second)
{...}
}
12.5.1 翻译泛型表达式
当程序调用泛型方法时,如果擦掉返回类型,编译器插入强制类型转换.例如,下面这个语句序列Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
擦除getFirst的返回类型后将返回Object类型.编译器自动插入Employee的强制类型转换.也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
对原始方法Pair.getFirst的调用
将返回的Object类型强制转换为Employee类型
当存取一个泛型域时也要插入强制类型转换.假设Pair类的first域和second域都是公有的.表达式:
Employee buddy = buddies.first;
也会在结果字节码中插入强制类型转换.
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;
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().clone();}
...
}
在擦除的类型中,有两个getSecond方法:
Date getSecond(); // define in DateInterval
Object getSecond(); // overrides the method defined in Pair to call the first method
不能这样编写Java代码,它们都没有参数,但是,在虚拟机中,用参数类型和返回类型确定一个方法.因此,编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能能够正确地处理这一情况.
注释:桥方法不仅用于泛型类型.在一个方法覆盖另一个方法时可指定一个更严格的返回类型.例如:
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.6 约束与局限性
在下面几节中,将阐述使用Java泛型时需要考虑的一些限制. 大多数限制都是由类型擦除引起的.12.6.1 不能用基本类型实例化类型参数
不能用类型参数代替基本类型,因此,没有Pair<double>,只有Pair<Double>.当然,其 原因是类型擦除.擦除之后,Pair类含有Object类型的域,而Object不能存储 double 值.这的确令人烦恼.但是,这样做与Java语言中基本类型的独立状态相一致.这并不是一个致命的缺陷--只有8种基本类型,当包装器类型不能接受替换时,可以使用独立的类和方法处理它们.
12.6.2 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型.因此,所有的类型查询只产生原始类型.例如:if (a instanceof Pair<String>) // error
实际上仅仅测试a是否是任意类型的一个Pair .下面的测试同样如此:
if (a instanceof Pair<T>) // error
或强制类型转换:
Pair<String> p = (Pair<String>)a;
要记住这一风险,无论何时使用 instanceof 或涉及泛型类型的强制类型转换表达式都会看到一个编译器警告.
同样的道理, getClass方法总是返回原始类型.例如:
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if (stringPair.getClass() == employeePair.getClass()) // they are equal
其比较的结果是 true,这是因为两次调用getClass都将返回Pair.class .
12.6.3 不能创建参数化类型的数组
不能实例化参数化类型的数组,例如:Pair<String>[] table = new Pair<String>[10]; // error
擦除之后,table的类型是Pair[],可以把它转换为Object[].
Object[] objarray = table;
数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个ArrayStoreException异常:
objarray[0] = "hello";
不过对于泛型类型,擦除会使得这种机制无效,以下赋值:
objarray[0] = new Pair<Employee>();
能够通过数组存储检查,不过仍然会导致一个类型错误.出于这个原因,不允许创建参数化类型的数组.
需要说明的是,只是不允许创建这些数组,而声明类型为Pair<String>[]的变量是合法的.
12.6.5 不能实例化类型变量
不能使用像 new T(...),new T[...]或T.class这样的表达式中的类型变量.例如下面的Pair<T>构造器就是非法的:public Pair() { first = new T(); second = new T(); } // error
类型擦除将T改变为Object,而且本意肯定不希望调用 new Object().