導語
在看spring-mvc的源碼的時候,看到在解析handler方法時,有關於獲取橋接方法代碼,不明白什么是橋接方法,經過查找資料,終於理解了什么是橋接方法。
什么是橋接方法
橋接方法是 JDK 1.5 引入泛型后,為了使Java的泛型方法生成的字節碼和 1.5 版本前的字節碼相兼容,由編譯器自動生成的方法。
我們可以通過Method.isBridge()
方法來判斷一個方法是否是橋接方法,在字節碼中橋接方法會被標記為ACC_BRIDGE
和ACC_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_BRIDGE
和ACC_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
類的源碼。實際上是通過判斷方法名、參數的個數以及泛型類型參數來獲取的。