深入理解泛型-重寫泛型類方法遇到的問題(涉及JVM反編譯位元組碼)

Ging發表於2024-09-02

當某類繼承一個泛型類,並且需要重寫其中的方法時,編譯器會自動新增一個橋方法,來保證多型的實現。

下面的程式碼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();

這種寫法在開發階段是不允許的,因為這兩個方法的方法名稱和引數完全一致,但對虛擬機器來說,因為採用引數型別和返回值型別確定一個方法,所以虛擬機器是可以區分的開的,不會出現錯誤。

實際上,這種橋方法會運用在重寫時子類的返回型別小於父類的情況,不僅僅運用在泛型中。

相關文章