1. 程式人生 > 其它 >Java 橋接方法

Java 橋接方法

Java 橋接方法

橋接方法概念

Java中的橋接方法(Bridge Method)是一種為了實現某些Java語言特性而由編譯器自動生成的方法。可以通過使用Java反射中 Method 類的 isBridge() 方法來判斷該方法是否是橋接方法。通過反射 Class.getMethod("") 取出的不是橋接方法。

在位元組碼檔案中,橋接方法會被標記為 ACC_BRIDGE 和 ACC_SYNTHETIC,其中 ACC_BRIDGE 表示該方法是由編譯器產生的橋接方法, ACC_SYNTHETIC 表示該方法是由編譯器自動生成。

什麼情況下會生成橋接方法

協變返回型別

協變返回型別是指子類方法的返回值型別不必嚴格等同於父類中被重寫的方法的返回值型別,而可以是更具體的型別,即子類重寫父類方法時,返回的型別可以是子類方法返回型別的子類。

看如下程式碼:

public class Parent {
    public Number get(){
        return 0;
    }
}

class Child1 extends Parent {

    public Number get(){
        return 1;
    }
}

class Child2 extends Parent {

    public Integer get(){
        return 2;
    }
}

執行以下命令,編譯並檢視位元組碼資料:

javac Parent.java
javap -c -v Child1
javap -c -v Child2

顯示結果如下(省略不重要的部分程式碼):

{
  Child1();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method Parent."<init>":()V
         4: return
      LineNumberTable:
        line 9: 0

  public java.lang.Number get();
    descriptor: ()Ljava/lang/Number;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: iconst_1
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 12: 0
}
{
  Child2();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method Parent."<init>":()V
         4: return
      LineNumberTable:
        line 16: 0

  public java.lang.Integer get();
    descriptor: ()Ljava/lang/Integer;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: iconst_2
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 19: 0

  public java.lang.Number get();
    descriptor: ()Ljava/lang/Number;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #3                  // Method get:()Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 16: 0
}

對比一下 Child1 和 Child2 的位元組碼可以看出,Child2 中多了一個 public java.lang.Number get(); 方法, 這個方法就是橋接方法,可以通過該方法的 flags 看到它含有 ACC_BRIDGE 標誌。這個方法就是一個橋接作用,從第30行可以看出它的方法內容,就是通過 invokevirtual 呼叫了自身的 get() 方法。為什麼需要通過這種方式來呼叫呢,是因為在JVM來說一個方法的簽名比Java語言多了一個返回值型別,也就是說,在Java語言中,認為只要方法名和引數列表一致就是同一個方法,而JVM則認為方法名、引數列表和返回型別全部一樣才是同一方法。而橋接方法的簽名和其父類的方法簽名一致,以此就實現了協變返回值型別。

泛型型別擦除

泛型是Java 1.5才引進的概念,在這之前是沒有泛型的概念的,但泛型程式碼能夠很好地和之前版本的程式碼很好地相容,這是因為,在編譯期間Java編譯器會將型別引數替換為其上界(型別引數中extends子句的型別),如果上界沒有定義,則預設為Object,這就叫做型別擦除。當一個子類在繼承(或實現)一個父類(或介面)的泛型方法時,在子類中明確指定了泛型型別,那麼在編譯時編譯器會自動生成橋接方法。

看如下程式碼:

public class Parent<T> {
    public Number get(T key){
        return 0;
    }
}

class Child1 extends Parent<String> {

    public Number get(String key){
        return 1;
    }
}

class Child2 extends Parent<String> {

    public Integer get(String key){
        return 2;
    }
}

執行以下命令,編譯並檢視位元組碼資料:

javac Parent.java
javap -c -v Child1
javap -c -v Child2

顯示結果如下(省略不重要的部分程式碼):

{
  Child1();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method Parent."<init>":()V
         4: return
      LineNumberTable:
        line 9: 0

  public java.lang.Number get(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/Number;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: iconst_1
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 12: 0

  public java.lang.Number get(java.lang.Object);
    descriptor: (Ljava/lang/Object;)Ljava/lang/Number;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #3                  // class java/lang/String
         5: invokevirtual #4                  // Method get:(Ljava/lang/String;)Ljava/lang/Number;
         8: areturn
      LineNumberTable:
        line 9: 0
}
{
  Child2();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method Parent."<init>":()V
         4: return
      LineNumberTable:
        line 16: 0

  public java.lang.Integer get(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/Integer;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: iconst_2
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 19: 0

  public java.lang.Number get(java.lang.Object);
    descriptor: (Ljava/lang/Object;)Ljava/lang/Number;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #3                  // class java/lang/String
         5: invokevirtual #4                  // Method get:(Ljava/lang/String;)Ljava/lang/Integer;
         8: areturn
      LineNumberTable:
        line 16: 0
}

對比一下 Child1 和 Child2 的位元組碼可以看出,Child2 中多了一個 public java.lang.Number get(java.lang.Object); 方法, 這個方法就是橋接方法,可以通過該方法的 flags 看到它含有 ACC_BRIDGE 標誌。還可以看出這個橋接方法的引數型別是 Object 型別,它與父類的引數型別是一致的,包括返回值也是與父類的型別是一致的。它的方法內容就是先進行型別檢查(第31行),然後再通過 invokevirtual 指令呼叫自身的 get(java.lang.String); 方法。

對於JVM來說,當它編譯時,它會直接把型別進行擦除,也就是會把類 Parent 變成如下形式:

public class Parent<Object> {
    public Number get(Object key){
        return 0;
    }
}

查詢橋接方法

使用 spring 提供的查詢方式:

method = BridgeMethodResolver.findBridgedMethod(method);