深入理解泛型-重写泛型类方法遇到的问题(涉及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();

这种写法在开发阶段是不允许的,因为这两个方法的方法名称和参数完全一致,但对虚拟机来说,因为采用参数类型和返回值类型确定一个方法,所以虚拟机是可以区分的开的,不会出现错误。

实际上,这种桥方法会运用在重写时子类的返回类型小于父类的情况,不仅仅运用在泛型中。

重点总结:当某类继承一个泛型类,并且需要重写其中的方法时,编译器会自动添加一个桥方法,来保证多态的实现。

posted @ 2024-09-02 11:20  Ging  阅读(33)  评论(0编辑  收藏  举报