java中的橋接方法


本文轉載自java中什么是bridge method(橋接方法)

導語

在看spring-mvc的源碼的時候,看到在解析handler方法時,有關於獲取橋接方法代碼,不明白什么是橋接方法,經過查找資料,終於理解了什么是橋接方法。

什么是橋接方法

橋接方法是 JDK 1.5 引入泛型后,為了使Java的泛型方法生成的字節碼和 1.5 版本前的字節碼相兼容,由編譯器自動生成的方法。

我們可以通過Method.isBridge()方法來判斷一個方法是否是橋接方法,在字節碼中橋接方法會被標記為ACC_BRIDGEACC_SYNTHETIC,其中ACC_BRIDGE用於說明這個方法是由編譯生成的橋接方法,ACC_SYNTHETIC說明這個方法是由編譯器生成,並且不會在源代碼中出現。可以查看jvm規范中對這兩個access_flag的解釋

有如下3個問題:

  • 什么時候會生成橋接方法
  • 為什么要生成橋接方法
  • 如何通過橋接方法獲取實際的方法

什么時候會生成橋接方法

那什么時候編譯器會生成橋接方法呢?可以查看JLS中的描述

就是說一個子類在繼承(或實現)一個父類(或接口)的泛型方法時,在子類中明確指定了泛型類型,那么在編譯時編譯器會自動生成橋接方法(當然還有其他情況會生成橋接方法,這里只是列舉了其中一種情況)。如下所示:

package com.mikan;

/**
 * @author Mikan
 * @date 2015-08-05 16:22
 */
public interface SuperClass<T> {

    T method(T param);

}

package com.mikan;

/**
 * @author Mikan
 * @date 2015-08-05 17:05
 */
public class SubClass implements SuperClass<String> {
    public String method(String param) {
        return param;
    }
}

來看一下SubClass的字節碼:

localhost:mikan mikan$ javap -c SubClass.class
Compiled from "SubClass.java"
public class com.mikan.SubClass implements com.mikan.SuperClass<java.lang.String> {
  public com.mikan.SubClass();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       5     0  this   Lcom/mikan/SubClass;

  public java.lang.String method(java.lang.String);
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: areturn
      LineNumberTable:
        line 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       2     0  this   Lcom/mikan/SubClass;
               0       2     1 param   Ljava/lang/String;

  public java.lang.Object method(java.lang.Object);
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #2                  // class java/lang/String
         5: invokevirtual #3                  // Method method:(Ljava/lang/String;)Ljava/lang/String;
         8: areturn
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       9     0  this   Lcom/mikan/SubClass;
               0       9     1    x0   Ljava/lang/Object;
}
localhost:mikan mikan$

SubClass只聲明了一個方法,而從字節碼可以看到有三個方法,第一個是無參的構造方法(代碼中雖然沒有明確聲明,但是編譯器會自動生成),第二個是我們實現的接口中的方法,第三個就是編譯器自動生成的橋接方法。可以看到flags包括了ACC_BRIDGEACC_SYNTHETIC,表示是編譯器自動生成的方法,參數類型和返回值類型都是Object。再看這個方法的字節碼,它把Object類型的參數強制轉換成了String類型,再調用在SubClass類中聲明的方法,轉換過來其實就是:

    public Object method(Object param) {
        return this.method(((String) param));
    }

也就是說,橋接方法實際是是調用了實際的泛型方法,來看看下面的測試代碼:

package com.mikan;

/**
 * @author Mikan
 * @date 2015-08-07 16:33
 */
public class BridgeMethodTest {

    public static void main(String[] args) throws Exception {
        SuperClass superClass = new SubClass();
        System.out.println(superClass.method("abc123"));// 調用的是實際的方法
        System.out.println(superClass.method(new Object()));// 調用的是橋接方法
    }

}

這里聲明了SuperClass類型的變量指向SubClass類型的實例,典型的多態。在聲明SuperClass類型的變量時,不指定泛型類型,那么在方法調用時就可以傳任何類型的參數,因為SuperClass中的方法參數實際上是Object類型,而且編譯器也不能發現錯誤。在運行時當參數類型不是SubClass聲明的類型時,會拋出類型轉換異常,因為這時調用的是橋接方法,而在橋接方法中會進行強制類型轉換,所以才會拋出類型轉換異常。上面的代碼輸出結果如下:

abc123
Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.String
	at com.mikan.SubClass.method(SubClass.java:7)
	at com.mikan.BridgeMethodTest.main(BridgeMethodTest.java:27)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:606)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)

如果我們在聲明SuperClass類型的變量就指定了泛型類型:

SuperClass<String> superClass = new SubClass();

當然這里類型只能是String,因為SubClass的泛型類型聲明是String類型的,如果指定其他類型,那么在編譯時就會錯誤,這樣就把類型檢查從運行時提前到了編譯時。這就是泛型的好處。
為什么要生成橋接方法
上面看到了編譯器在什么時候會生成橋接方法,那為什么要生成橋接方法呢?

在java1.5以前,比如聲明一個集合類型:

List list = new ArrayList();

那么往list中可以添加任何類型的對象,但是在從集合中獲取對象時,無法確定獲取到的對象是什么具體的類型,所以在1.5的時候引入了泛型,在聲明集合的時候就指定集合中存放的是什么類型的對象:

List<String> list = new ArrayList<String>();

那么在獲取時就不必擔心類型的問題,因為泛型在編譯時編譯器會檢查往集合中添加的對象的類型是否匹配泛型類型,如果不正確會在編譯時就會發現錯誤,而不必等到運行時才發現錯誤。因為泛型是在1.5引入的,為了向前兼容,所以會在編譯時去掉泛型(泛型擦除),但是我們還是可以通過反射API來獲取泛型的信息,在編譯時可以通過泛型來保證類型的正確性,而不必等到運行時才發現類型不正確。由於java泛型的擦除特性,如果不生成橋接方法,那么與1.5之前的字節碼就不兼容了。如前面的SuperClass中的方法,實際在編譯后的字節碼如下:

localhost:mikan mikan$ javap -c -v SuperClass.class
Classfile /Users/mikan/Documents/workspace/project/algorithm/target/classes/com/mikan/SuperClass.class
  Last modified 2015-8-7; size 251 bytes
  MD5 checksum 2e2530041f1f83aaf416a2ca3af9b7e3
  Compiled from "SuperClass.java"
public interface com.mikan.SuperClass<T extends java.lang.Object>
  Signature: #7                           // <T:Ljava/lang/Object;>Ljava/lang/Object;
  SourceFile: "SuperClass.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
   #1 = Class              #10            //  com/mikan/SuperClass
   #2 = Class              #11            //  java/lang/Object
   #3 = Utf8               method
   #4 = Utf8               (Ljava/lang/Object;)Ljava/lang/Object;
   #5 = Utf8               Signature
   #6 = Utf8               (TT;)TT;
   #7 = Utf8               <T:Ljava/lang/Object;>Ljava/lang/Object;
   #8 = Utf8               SourceFile
   #9 = Utf8               SuperClass.java
  #10 = Utf8               com/mikan/SuperClass
  #11 = Utf8               java/lang/Object
{
  public abstract T method(T);
    flags: ACC_PUBLIC, ACC_ABSTRACT
    Signature: #6                           // (TT;)TT;
}
localhost:mikan mikan$

通過Signature: #7 // <T:Ljava/lang/Object;>Ljava/lang/Object;可以看到,在編譯完成后泛型實際上就成了Object了,所以方法實際上成了

public abstract Object method(Object param);

SubClass實現了SuperClass這個接口,如果不生成橋接方法,那么SubClass就沒有實現接口中聲明的方法,語義就不正確了,所以編譯器才會自動生成橋接方法,來保證兼容性。

如何通過橋接方法獲取實際的方法

我們在通過反射進行方法調用時,如果獲取到橋接方法對應的實際的方法呢?可以查看spring中org.springframework.core.BridgeMethodResolver類的源碼。實際上是通過判斷方法名、參數的個數以及泛型類型參數來獲取的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM