在Java编程语言中,泛型信息仅在编译期有效,当代码进入JVM运行时环境时,所有与泛型相关的类型信息都会被移除,这一过程被称为类型擦除。
* 完全类型擦除:当类声明中未对泛型参数施加任何约束时,擦除后将统一转换为Object类型。例如,和
都会变成Object类型。
* 受限类型擦除:若泛型参数设定了类型边界(上限或下限),擦除后会保留边界类型。比如会被替换为Person,而
则仍会转为Object。
桥接方法:解决多态冲突的关键
类型擦除会引发多态性问题,Java虚拟机通过引入桥接方法这一巧妙机制来解决这个矛盾。
实例分析
假设存在以下泛型基类:
class GenericPair {
private T storedValue;
public T retrieveValue() {
return storedValue;
}
public void storeValue(T input) {
this.storedValue = input;
}
}
其派生类定义如下:
class DatePair extends GenericPair {
@Override
public void storeValue(Date input) {
super.storeValue(input);
}
@Override
public Date retrieveValue() {
return super.retrieveValue();
}
}
开发者本意是通过继承将基类的泛型限定为Date类型,期望基类方法都使用Date作为参数和返回类型。然而经过类型擦除后,基类实际变为:
class GenericPair {
private Object storedValue;
public Object retrieveValue() {
return storedValue;
}
public void storeValue(Object input) {
this.storedValue = input;
}
}
观察派生类的方法签名:storeValue方法的参数类型在基类中是Object,而在派生类中是Date。按照常规继承规则,这应该构成方法重载而非重写。测试代码验证:
public static void main(String[] args) {
DatePair pair = new DatePair();
pair.storeValue(new Date()); // 正常执行
pair.storeValue(new Object()); // 编译报错
}
如果这是方法重载,派生类应该同时包含Object和Date版本的storeValue方法,但实际上并不存在Object版本的方法。这表明确实是发生了方法重写。
问题本质
理想情况下,指定泛型参数为Date后,基类应该变为:
class GenericPair {
private Date storedValue;
public Date retrieveValue() {
return storedValue;
}
public void storeValue(Date input) {
this.storedValue = input;
}
}
但由于JVM的限制,泛型信息被擦除为Object类型,导致原本计划的方法重写变成了方法重载,这就产生了多态性实现的障碍。JVM通过桥接方法这一特殊机制来化解这个矛盾。
实现机制
使用javap工具反编译DatePair类的字节码,可以看到:
class DatePair extends GenericPair {
// 构造方法...
public void storeValue(Date); // 开发者显式重写的方法
public Date retrieveValue(); // 开发者显式重写的方法
// 编译器自动生成的桥接方法
public Object retrieveValue() {
return invokevirtual #28 // 调用Date版本的retrieveValue
}
public void storeValue(Object) {
checkcast #26 // 类型检查
invokevirtual #30 // 调用Date版本的storeValue
}
}
反编译结果显示,实际存在四个方法。除了开发者显式重写的两个方法外,编译器还自动生成了两个桥接方法。关键点在于:
1. 桥接方法使用Object作为参数/返回类型,真正实现了对基类方法的重写
2. 开发者标注@Override的方法实际上并未直接重写基类方法
3. 桥接方法内部通过类型检查和转换后,调用开发者定义的特定类型版本
值得注意的是,派生类中同时存在Object retrieveValue()和Date retrieveValue()方法。在常规Java编码中,这种仅返回值类型不同的方法定义是无法通过编译的。但JVM层面允许这种特殊处理,因为虚拟机通过完整的参数类型和返回类型来区分方法,这使得编译器能够为实现泛型多态性进行这种特殊处理。