什么是橋接方法?
Java中的橋接方法(Bridge Method)是一種為了實(shí)現(xiàn)某些Java語言特性而由編譯器自動(dòng)生成的方法锣尉。
我們可以通過Method
類的isBridge
方法來判斷一個(gè)方法是否是橋接方法。
在字節(jié)碼文件中决采,橋接方法會(huì)被標(biāo)記為ACC_BRIDGE
和ACC_SYNTHETIC
自沧,其中ACC_BRIDGE
用于表示該方法是由編譯器產(chǎn)生的橋接方法,ACC_SYNTHETIC
用于表示該方法是由編譯器自動(dòng)生成。
什么時(shí)候生成橋接方法拇厢?
為了實(shí)現(xiàn)哪些Java語言特性會(huì)生成橋接方法爱谁?最常見的兩種情況就是協(xié)變返回值類型和類型擦除,因?yàn)樗鼈儗?dǎo)致了父類方法的參數(shù)和實(shí)際調(diào)用的方法參數(shù)類型不一致孝偎。下面我們通過兩個(gè)例子更好地理解一下访敌。
協(xié)變返回類型
協(xié)變返回類型是指子類方法的返回值類型不必嚴(yán)格等同于父類中被重寫的方法的返回值類型,而可以是更 "具體" 的類型衣盾。
在Java 1.5添加了對(duì)協(xié)變返回類型的支持寺旺,即子類重寫父類方法時(shí),返回的類型可以是子類方法返回類型的子類势决。下面看一個(gè)例子:
public class Parent {
Number get() {
return 1;
}
}
public class Child extends Parent {
@Override
Integer get() {
return 1;
}
}
Child類重寫其父類Parent的get方法阻塑,Parent的get方法返回類型為Number,而Child類中g(shù)et方法返回類型為Integer徽龟。
將這段代碼進(jìn)行編譯叮姑,再反編譯:
javac Child.java
javap -v -c Child.class
結(jié)果如下:
public class Child extends Parent
......省略部分結(jié)果......
java.lang.Integer get();
descriptor: ()Ljava/lang/Integer;
flags:
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 5: 0
java.lang.Number get();
descriptor: ()Ljava/lang/Number;
flags: 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 1: 0
從上面的結(jié)果可以看到,有一個(gè)方法java.lang.Number get(), 在源碼中是沒有出現(xiàn)過的据悔,是由編譯器自動(dòng)生成的传透,該方法被標(biāo)記為ACC_BRIDGE
和ACC_SYNTHETIC
,就是我們前面所說的橋接方法极颓。
這個(gè)方法就起了一個(gè)橋接的作用朱盐,它所做的就是把對(duì)自身的調(diào)用通過invokevirtual
指令再調(diào)用方法java.lang.Integer get()。
編譯器這么做的原因是什么呢菠隆?因?yàn)樵贘VM方法中兵琳,返回類型也是方法簽名的一部分,而橋接方法的簽名和其父類的方法簽名一致骇径,以此就實(shí)現(xiàn)了協(xié)變返回值類型躯肌。
類型擦除
泛型是Java 1.5才引進(jìn)的概念,在這之前是沒有泛型的概念的破衔,但泛型代碼能夠很好地和之前版本的代碼很好地兼容清女,這是為什么呢?
這是因?yàn)槲福诰幾g期間Java編譯器會(huì)將類型參數(shù)替換為其上界(類型參數(shù)中extends子句的類型)嫡丙,如果上界沒有定義,則默認(rèn)為Object读第,這就叫做類型擦除曙博。
當(dāng)一個(gè)子類在繼承(或?qū)崿F(xiàn))一個(gè)父類(或接口)的泛型方法時(shí),在子類中明確指定了泛型類型怜瞒,那么在編譯時(shí)編譯器會(huì)自動(dòng)生成橋接方法父泳,例如:
public class Parent<T> {
void set(T t) {
}
}
public class Child extends Parent<String> {
@Override
void set(String str) {
}
}
Child類在繼承其父類Parent的泛型方法時(shí),明確指定了泛型類型為String,將這段代碼進(jìn)行編譯尘吗,再反編譯:
public class Child extends Parent<java.lang.String>
......省略部分結(jié)果......
void set(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags:
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 5: 0
void set(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: 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 set:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 1: 0
從上面的結(jié)果可以看到逝她,有一個(gè)方法void set(java.lang.Object), 在源碼中是沒有出現(xiàn)過的,是由編譯器自動(dòng)生成的睬捶,該方法被標(biāo)記為ACC_BRIDGE
和ACC_SYNTHETIC
,就是我們前面所說的橋接方法近刘。
這個(gè)方法就起了一個(gè)橋接的作用擒贸,它所做的就是把對(duì)自身的調(diào)用通過invokevirtual
指令再調(diào)用方法void set(java.lang.String)。
編譯器這么做的原因是什么呢觉渴?因?yàn)镻arent類在類型擦除之后介劫,變成這樣:
public class Parent<Object> {
void set(Object t) {
}
}
編譯器為了讓子類有一個(gè)與父類的方法簽名一致的方法,就在子類自動(dòng)生成一個(gè)與父類的方法簽名一致的橋接方法案淋。
如何獲取橋接方法的實(shí)際方法
在Spring Framework中已經(jīng)實(shí)現(xiàn)了獲取橋接方法的實(shí)際方法的功能座韵,就在spring-core模塊中的BridgeMethodResolver類中,像這樣直接使用就行了:
method = BridgeMethodResolver.findBridgedMethod(method);
findBridgedMethod方法是怎么實(shí)現(xiàn)的呢踢京?我們來分析一下源碼(spring-core的版本為5.2.8.RELEASE):
public static Method findBridgedMethod(Method bridgeMethod) {
// 如果不是橋連方法誉碴,就直接返回原方法。
if (!bridgeMethod.isBridge()) {
return bridgeMethod;
}
// 先從本地緩存讀取瓣距,緩存中有則直接返回黔帕。
Method bridgedMethod = cache.get(bridgeMethod);
if (bridgedMethod == null) {
List<Method> candidateMethods = new ArrayList<>();
// 以方法名稱和入?yún)€(gè)數(shù)相等為篩選條件。
MethodFilter filter = candidateMethod ->
isBridgedCandidateFor(candidateMethod, bridgeMethod);
// 遞歸該類及其所有父類上的所有方法蹈丸,符合篩選條件就添加進(jìn)來成黄。
ReflectionUtils.doWithMethods(bridgeMethod.getDeclaringClass()
, candidateMethods::add, filter);
if (!candidateMethods.isEmpty()) {
// 如果符合篩選條件的方法個(gè)數(shù)為1,則直接采用逻杖;
// 否則奋岁,調(diào)用searchCandidates方法再次篩選。
bridgedMethod = candidateMethods.size() == 1 ?
candidateMethods.get(0) :
searchCandidates(candidateMethods, bridgeMethod);
}
// 如果找不到實(shí)際方法荸百,則返回原來的橋連方法闻伶。
if (bridgedMethod == null) {
// A bridge method was passed in but we couldn't find the bridged method.
// Let's proceed with the passed-in method and hope for the best...
bridgedMethod = bridgeMethod;
}
// 把查找的結(jié)果放入內(nèi)存緩存。
cache.put(bridgeMethod, bridgedMethod);
}
return bridgedMethod;
}
我們?cè)倏匆幌略俅魏Y選的searchCandidates方法是如何實(shí)現(xiàn)的:
private static Method searchCandidates(List<Method> candidateMethods, Method bridgeMethod) {
if (candidateMethods.isEmpty()) {
return null;
}
Method previousMethod = null;
boolean sameSig = true;
// 遍歷候選方法的列表
for (Method candidateMethod : candidateMethods) {
// 對(duì)比橋接方法的泛型類型參數(shù)和候選方法是否匹配管搪,如果匹配則直接返回該候選方法虾攻。
if (isBridgeMethodFor(bridgeMethod, candidateMethod, bridgeMethod.getDeclaringClass())) {
return candidateMethod;
}
else if (previousMethod != null) {
// 如果不匹配,則判斷所有候選方法的參數(shù)列表是否相等更鲁。
sameSig = sameSig && Arrays.equals(candidateMethod.getGenericParameterTypes()
, previousMethod.getGenericParameterTypes());
}
previousMethod = candidateMethod;
}
// 如果所有候選方法的參數(shù)列表全相等霎箍,則返回第一個(gè)候選方法。
return (sameSig ? candidateMethods.get(0) : null);
}
總結(jié)以上源碼就是澡为,通過判斷方法名漂坏、參數(shù)的個(gè)數(shù)以及泛型類型參數(shù)來獲取橋接方法的實(shí)際方法。
最后,謝謝你這么帥顶别,還給我點(diǎn)贊和關(guān)注谷徙。