[譯] Java 橋接方法詳解

kezhenxu94發表於2019-02-27

Java 中的橋接方法是一種合成方法,在實現某些 Java 語言特性的時候是很有必要的。最為人熟知的例子就是協變返回值型別和泛型擦除後導致基類方法的引數與實際呼叫的方法引數型別不一致。

看一下以下的例子:

public class SampleOne {
    public static class A<T> {
        public T getT() {
            return null;
        }
    }

    public static class  B extends A<String> {
        public String getT() {
            return null;
        }
    }
}
複製程式碼

事實上這就是一個協變返回型別的例子,泛型擦除後將會變成類似於下面這樣的程式碼段:

public class SampleOne {
    public static class A {
        public Object getT() {
            return null;
        }
    }

    public static class  B extends A {
        public String getT() {
            return null;
        }
    }
}
複製程式碼

在將編譯後的位元組碼反編譯後,類 B 會是這樣子的:

public class SampleOne$B extends SampleOne$A {
public SampleOne$B();
...
public java.lang.String getT();
Code:
0:   aconst_null
1:   areturn
public java.lang.Object getT();
Code:
0:   aload_0
1:   invokevirtual   #2; // 呼叫 getT:()Ljava/lang/String;
4:   areturn
}
複製程式碼

從上面可以看到,有一個新合成的方法 java.lang.Object getT(), 這在原始碼中是沒有出現過的。這個方法就起了一個橋接的作用,它所做的就是把對自身的呼叫委託給方法 jva.lang.String getT()。編譯器不得不這麼做,因為在 JVM 方法中,返回型別也是方法簽名的一部分,而橋接方法的建立就正好是實現協變返回值型別的方式。

現在再看一看下面和泛型相關的例子:

public class SampleTwo {
    public static class A<T> {
        public T getT(T args) {
            return args;
        }
    }

    public static class B extends A<String> {
        public String getT(String args) {
            return args;
        }
    }
}
複製程式碼

編譯後類 B 會變成下面這樣子:

public class SampleThree$B extends SampleThree$A{
public SampleThree$B();
...
public java.lang.String getT(java.lang.String);
Code:
0:   aload_1
1:   areturn

public java.lang.Object getT(java.lang.Object);
Code:
0:   aload_0
1:   aload_1
2:   checkcast       #2; //class java/lang/String
5:   invokevirtual   #3; //Method getT:(Ljava/lang/String;)Ljava/lang/String;
8:   areturn
}
複製程式碼

這裡的橋接方法覆蓋了(override)基類 A 的方法,不僅使用字串引數將對自身的呼叫委派給基類 A 的方法,同時也執行了一個到 java.lang.String 的型別轉換檢測(#2)。這就意味著如果你執行下面這樣的程式碼,忽略編譯器的“未檢”(unchecked)警告,結果會是從橋接方法那裡丟擲異常 ClassCastException

A a = new B();
a.getT(new Object()));
複製程式碼

以上例子就是橋接方法最為人熟知的兩種使用場景,但至少還有一種使用案例,就是橋接方法被用於“改變”基類可見性。考慮以下示例程式碼,猜測一下編譯器是否需要建立一個橋接方法:

package samplefour;

public class SampleFour {
    static class A {
        public void foo() {
        }
    }
    public static class C extends A {

    }
    public static class D extends A {
        public void foo() {
        }
    }
}
複製程式碼

如果你反編譯 C 類,你將會看到有 foo 方法,它覆蓋了基類的方法並把對自身的呼叫委託給它(基類的方法):

public class SampleFour$C extends SampleFour$A{
...
public void foo();
Code:
0:   aload_0
1:   invokespecial   #2; //Method SampleFour$A.foo:()V
4:   return

}
複製程式碼

編譯器需要這樣的方法,因為 A 類不是公開的,在 A 類所在包之外是不可見的,但是 C 類是公開的,它所繼承來的所有方法在所在包之外也都應該是可見的。需要注意的是,D 類不會有橋接方法生成,因為它覆蓋了 foo 方法,因此沒有必要“提升”其可見性。
這種橋接方法似乎是由於這個 bug (在 Java 6 被修復)才引入的。這意味著在 Java 6 之前是不會生成這樣橋接方法的,那麼 C#foo 就不能夠在它所在包之外使用反射呼叫,以致於下面這樣的程式碼在 Java 版本小於 1.6 時會報 IllegalAccessException 異常。

package samplefive;
...
SampleFour.C.class.getMethod("foo").invoke(new SampleFour.C());
...
複製程式碼

不使用反射機制,正常呼叫的話是起作用的。

可能還有其他使用橋接方法的案例,但沒有相關的資訊來源。此外,關於橋接方法也沒有明確的定義,儘管你可以很容易的猜測出來,像以上的示例是相當明顯的,但如果有一些規範把橋接方法說明清楚的話就更好了。儘管自 Java 5 開始 Method#isBridge() 方法 就是公開的反射 API 了,橋接的標誌也是位元組碼檔案格式中的一部分,但 Java 虛擬機器和 Java 語言規範都始終沒有任何關於橋接方法的確切文件,也沒有提供關於編譯器何時/如何使用橋接方法的任何規則。我所能找到的全部就是在這裡的“討論區”的引用。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章