虚拟机没有泛型类对象——所有对象都属于普通类。所以泛型类会在编译时被翻译成普通类。
类型擦除
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删除类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型(无线定的变量用Object)。
例如,Pair的原始类型如下所示:
public class Pair
{
private Object first;
private Object second;
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; }
}
因为T是一个无限定的变量,所以直接用Object替换。结果是一个普通的类,这与泛型引入Java语言之前的实现是类似的。
原始类型用第一个限定的类型变量来替换,如果没有给定限定用Object替换。如果声明了一个不同的类型:
public class Interval(T extends Comparable & Serizalizable> 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) { ... }
}
如果将Interavel切换限定为:class Interavel<T extends Serializable & Comparable>
,原始类型将用Serializable
替换T
,编译器在必要时要向Comparable
插入强制类型转换。为了提高效率,应该将标签(tagging)接口放在便捷列表的末尾。
翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。例如:
Pair<Employee> budies = ...;
Employee buddy = (Employee) buddies.getFirst();
当存取一个泛型域时也要插入强制类型转换。例如:
Employee buddy = (Employee) buddies.first;
翻译泛型方法
泛型方法中的类型擦除会去掉类型参数,只留下限定类型。例如:
// public static <T extends Comparable> T min(T[] a)
public static Comparable min(Comparable[] a)
类型参数T已经被擦除,只留下限定类型Comparable
。
方法的擦除会带来复杂的问题。示例:
class DateInterval extends Pair<LocalDate>
{
public void setSecond(LocalDate second)
{
if(second.compareTo(getFirst()) >= 0)
{
super.setSecond(second);
}
}
...
}
在DateInterval
类中,我们想覆盖Pair
的setSecond
方法来确保第二个值永远不小于第一个值。DateInterval
类擦除后变成:
class DateInterval extends Pair //after erasure
{
public void setSecond((LocalDate second) { ... }
...
}
这个时候问题就出来了,我们发现DateInterval
类中的setSecond
方法参数与Pair
类中的setSecond
方法参数不一样。这样,DateInterval
类就有两个setSecond
方法:
// 自己的
public void setSecond(LocalDate second) { ... }
// 从Pair父类中继承的
public void setSecond(Object second) { ... }
我们原本想通过在DateInterval
类中重写父类Pair
的setSecond
方法来实现继承多态性,可是类型擦除后,变成了重载。考虑一下代码:
Pair<LocalDate> pair = new DateInterval(...);
pair.setSecond(LocalDate.now());
pair
是Pair<LocalDate>
类型,当在pair
对象上调用setSecond(LocalDate.now())
时,会执行Pair
的setSecond(Object second)
方法,由于pair
引用DateInterval
对象,如果DateInterval
类的setSecond
方法覆盖了父类的该方法,应该调用DateInterval.setSecond(LocalDate)
。但DateInerval
没有重写而是重载了Pair.setSecond(Object)
方法,也就是说是类型擦除与多态发送了冲突。(更多说明参见:【java】–泛型-类型擦除与多态的冲突和解决方法_Java_qfzhangwei的专栏-优快云博客)
要解决这个问题,就需要编译器在DateInterval
类中生成一个桥方法(bridge method):
public void setSecond(Object second)
{
setSecond((Date) second);
}
(编译器在编译阶段自动生成)
跟踪下列语句的执行:
pair.setSecond(LocalDate.now());
变量pair
已经声明为类型Pair<LocalDate>
,并且这个类型只有一个简单的方法叫setSecond
,即setSecond(Object)
。虚拟机用pair
引用的对象调用这个方法。这个对象是DateInterval
类型的,因而将会调用DateInterval.setSecond(Object)
方法。这个方法是合成的桥方法。它调用DateInterval.setSecond(Date)
,这正是我们所期望的操作效果。
桥方法可能会变得十分奇怪。假设DateInerval
也覆盖了getSecond
方法:
class DateInterval extends Pair<LocalDate>
{
public LocalDate getSecond() { ... }
...
}
在DateInterval
类中,有两个getSecond
方法:
LocalDate getSecond() // defined 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() // defaine above
Object clone() // synthesized bridge method, overrides Object.clone
合成的桥方法调用了新定义的方法。
总之,需要记住有关Java泛型转换的事实:
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都用它们的限定类型替换。
- 桥方法被合成来保持多台。
- 为保持类型安全性,必要时插入强制类型转换。
调用遗留代码
设计Java泛型类型时,主要目标是允许泛型代码和遗留代码之间能互操作。
下面示例中,要想设置一个JSlider标签,可以使用方法:
void setLabelTable(Dictionary table)
这里的Dictionary
是一个原始类型,因为实现JSlider
类时Java中还不存在泛型。不过,填充Dictionary
时,要使用泛型类型。
Dictionary<Integer, Component> labelTable = new Hashtable<>();
lableTable.put(0, new JLable(new ImageIcon("nine.gif")));
lableTable.put(20, new JLable(new ImageIcon("ten.gif")));
...
将Dictionary<Integer, Component>
对象传递给setLabelTable
时,编译器会发送一个警告。
slider.setLabelTable(labelTable); // Warning
毕竟,编译器无法确定setLabelTable
可能会对Dictionary
对象做什么操作。这个方法可能会用字符串替换所有的关键字。这就打破了关键字类型为整数(Integer)的承诺,未来的操作有可能会产生强制类型转换的异常。
这个警告对操作不会产生什么影响,最多考虑一下JSlider
有可能用Dictionary
·对象做什么就可以了。这里十分清楚,JSlider
只阅读这个信息,因此可以忽略这个警告。
在查看了警告之后,可以利用注解(annotation)使之消失。注解必须放在生成这个警告的代码所在的方法之前,如下:
@SuppressWarning("unchecked")
Dictionary<Integer, Components> labelTable = slider.getLabelTable(); // No warning
或者,可以标注整个方法,这个注解会关闭对方法中所有代码的检查。