深入理解泛型-重写泛型类方法遇到的问题(涉及JVM反编译字节码)
下面的代码DateInterval类想重写父类Pair<LocalDate>中的setSecond方法,保证设置的第二个日期要在第一个日期之后,不能出现second早于first的情况。这里存在两种写法,报错写法使用的是Object作为参数类型,成功写法使用LocalDate。
public class Pair<T>{
private T first;
private T second;
public T getFirst(){...}
public T getSecond(){...}
public void setFirst(T first){...}
public void setSecond(T second){...}
}
public class DateInterval extends Pair<LocalDate>{
//报错写法
public void setSecond(Object secondDate){
if (secondDate instanceof LocalDate) {
if (((LocalDate)secondDate).compareTo(getFirst()) >= 0) {
super.setSecond((LocalDate) secondDate);
}
}
}
//成功写法
public void setSecond(LocalDate secondDate){
if (secondDate.compareTo(getFirst()) >= 0){
super.setSecond(secondDate);
}
}
public static void main(String[] args) {
Pair<LocalDate> pair = new DateInterval(LocalDate.of(1991,8,16),LocalDate.of(1992,8,16));
pair.setSecond(LocalDate.of(1970,8,16));
}
}
第一个问题:为什么选择使用Object呢?
因为了解泛型擦除原理,所以在重写方法时选择了Object作为参数类型,以为可以成功覆写却失败。
第二个问题:为什么选择使用LocalDate呢?
逻辑上讲,泛型擦除对开发者是不可见的,开发者使用Pair<LocalDate>后,自然认为字段的类型是LocalDate,所以使用LocalDate进行覆写合情合理。
为了解决报错疑惑,我们先来一步一步分析下成功写法下的整个流程,即:main方法中的下面这句是如何执行的?
pair.setSecond(LocalDate.of(1970,8,16));
因为使用父类型Pair作为pair变量的静态类型,所以在调用setSecond方法时,编译器会将其翻译为调用Pair类的setSecond(Object)方法等待多态执行:
39: invokevirtual #3 // Method com/company/Pair.setSecond:(Ljava/lang/Object;)V
虚拟机执行这行指令时,会采用动态分派来挑选出实际要执行的代码段,简而言之,虚拟机会先从DateInterval类开始找起,找能匹配描述符 setSecond:(Ljava/lang/Object;)V 的方法,但是DateInterval中其实只有setSecond:(Ljava/time/LocalDate)V 这个方法,并不存在setSecond:(Ljava/lang/Object;)V,所以DateInterval没有匹配成功。虚拟机会继续向上找,找到了DateInterval的父类Pair,并且找到了匹配成功的方法,所以实际上这行代码其实是执行父类Pair.setSecond(Object)。
注:描述符为描述方法的一种格式,方法名 参数列表 返回值这样的格式,见上段的例子setSecond。
那么此时问题出现了,本想通过重写来实现功能扩展,结果还是调用了父类的Object为参数的方法,为了解决这个问题,编译器设计了桥方法,即编译器在DateInterval类中自动插入一个setSecond:(Ljava/lang/Object;)V方法,方法内部调用的是setSecond:(Ljava/time/LocalDate)V 这个方法来实现搭桥引路,即下面反编译字节码中第五行指令:
5: invokevirtual #11 // Method setSecond:(Ljava/time/LocalDate;)V
public void setSecond(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #10 // class java/time/LocalDate
5: invokevirtual #11 // Method setSecond:(Ljava/time/LocalDate;)V
了解了以上问题,就可以清楚报错写法的错误原因了。简单来说就是,编译器会自动添加一个桥方法,如果开发者再定义一个参数为Object的方法,那么DateInterval中就出现了两个描述符完全一样的两个方法。
假如允许这样的写法存在,那么虚拟机执行的时候应该用哪个方法呢?虚拟机也会迷惑(虚拟机执行的时候使用参数类型和返回值类型确定一个方法),所以不允许这样做。
考虑DateInterval重写getSecond方法,那么依照上面的分析,虚拟机也会自动生成一个返回值为Object类型的getSecond方法,并且在方法内部调用DateInterval重写的getSecond方法,DateInterval内部实际上会有两个getSecond方法:
LocalDate getSecond();
Object getSecond();
这种写法在开发阶段是不允许的,因为这两个方法的方法名称和参数完全一致,但对虚拟机来说,因为采用参数类型和返回值类型确定一个方法,所以虚拟机是可以区分的开的,不会出现错误。
实际上,这种桥方法会运用在重写时子类的返回类型小于父类的情况,不仅仅运用在泛型中。
重点总结:当某类继承一个泛型类,并且需要重写其中的方法时,编译器会自动添加一个桥方法,来保证多态的实现。