Java 橋接方法詳解
Java 中的橋接方法是一種合成方法炸枣,在實現(xiàn)某些 Java 語言特性的時候是很有必要的即横。最為人熟知的例子就是協(xié)變返回值類型和泛型擦除后導致基類方法的參數(shù)與實際調(diào)用的方法參數(shù)類型不一致礁遵。
看一下以下的例子:
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;
}
}
}
事實上這就是一個協(xié)變返回類型的例子单起,泛型擦除后將會變成類似于下面這樣的代碼段:
public class SampleOne {
public static class A {
public Object getT() {
return null;
}
}
public static class B extends A {
public String getT() {
return null;
}
}
}
在將編譯后的字節(jié)碼反編譯后阳谍,類 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; // 調(diào)用 getT:()Ljava/lang/String;
4: areturn
}
從上面可以看到蛀柴,有一個新合成的方法 java.lang.Object getT()
, 這在源代碼中是沒有出現(xiàn)過的。這個方法就起了一個橋接的作用矫夯,它所做的就是把對自身的調(diào)用委托給方法 jva.lang.String getT()
鸽疾。編譯器不得不這么做,因為在 JVM 方法中训貌,返回類型也是方法簽名的一部分制肮,而橋接方法的創(chuàng)建就正好是實現(xiàn)協(xié)變返回值類型的方式。
現(xiàn)在再看一看下面和泛型相關(guān)的例子:
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
的方法递沪,不僅使用字符串參數(shù)將對自身的調(diào)用委派給基類 A
的方法豺鼻,同時也執(zhí)行了一個到 java.lang.String
的類型轉(zhuǎn)換檢測(#2)。這就意味著如果你運行下面這樣的代碼款慨,忽略編譯器的“未檢”(unchecked)警告儒飒,結(jié)果會是從橋接方法那里拋出異常 ClassCastException
。
A a = new B();
a.getT(new Object()));
以上例子就是橋接方法最為人熟知的兩種使用場景檩奠,但至少還有一種使用案例桩了,就是橋接方法被用于“改變”基類可見性附帽。考慮以下示例代碼井誉,猜測一下編譯器是否需要創(chuàng)建一個橋接方法:
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
方法,它覆蓋了基類的方法并把對自身的調(diào)用委托給它(基類的方法):
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
類是公開的欠啤,它所繼承來的所有方法在所在包之外也都應(yīng)該是可見的荚藻。需要注意的是,D
類不會有橋接方法生成洁段,因為它覆蓋了 foo
方法应狱,因此沒有必要“提升”其可見性。
這種橋接方法似乎是由于這個 bug(在 Java 6 被修復(fù))才引入的祠丝。這意味著在 Java 6 之前是不會生成這樣橋接方法的疾呻,那么 C#foo
就不能夠在它所在包之外使用反射調(diào)用,以致于下面這樣的代碼在 Java 版本小于 1.6 時會報 IllegalAccessException
異常写半。
package samplefive;
...
SampleFour.C.class.getMethod("foo").invoke(new SampleFour.C());
...
不使用反射機制岸蜗,正常調(diào)用的話是起作用的。
可能還有其他使用橋接方法的案例叠蝇,但沒有相關(guān)的資料璃岳。此外,關(guān)于橋接方法也沒有明確的定義悔捶,盡管你可以很容易的猜測出來铃慷,像以上的示例是相當明顯的,但如果有一些規(guī)范把橋接方法說明清楚的話就更好了蜕该。盡管自 Java 5 開始 Method#isBridge()
方法 就是公開的反射 API 了犁柜,橋接的標志也是字節(jié)碼文件格式中的一部分,但 Java 虛擬機和 Java 語言規(guī)范都始終沒有任何關(guān)于橋接方法的確切文檔堂淡,也沒有提供關(guān)于編譯器何時/如何使用橋接方法的任何規(guī)則馋缅。我所能找到的全部引用都是來自這里的“討論區(qū)”。
掘金翻譯計劃 是一個翻譯優(yōu)質(zhì)互聯(lián)網(wǎng)技術(shù)文章的社區(qū)绢淀,文章來源為 掘金 上的英文分享文章萤悴。內(nèi)容覆蓋 Android、iOS更啄、前端稚疹、后端居灯、區(qū)塊鏈祭务、產(chǎn)品内狗、設(shè)計、人工智能等領(lǐng)域义锥,想要查看更多優(yōu)質(zhì)譯文請持續(xù)關(guān)注 掘金翻譯計劃柳沙、官方微博、知乎專欄拌倍。