[譯] Java 橋接方法詳解

Java 中的橋接方法是一種合成方法,在實現某些 Java 語言特性的時候是頗有必要的。最爲人熟知的例子就是協變返回值類型和泛型擦除後致使基類方法的參數與實際調用的方法參數類型不一致。html

看一下如下的例子:前端

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;
        }
    }
}
複製代碼

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

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

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

在將編譯後的字節碼反編譯後,類 B 會是這樣子的:android

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 方法中,返回類型也是方法簽名的一部分,而橋接方法的建立就正好是實現協變返回值類型的方式。ios

如今再看一看下面和泛型相關的例子:git

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 會變成下面這樣子:github

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)警告,結果會是從橋接方法那裏拋出異常 ClassCastExceptionexpress

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 方法,它覆蓋了基類的方法並把對自身的調用委託給它(基類的方法):api

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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索