自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Java底層知識(shí):什么是 “橋接方法” ?

開(kāi)發(fā) 前端
筆者羅列了幾種編譯器為我們自動(dòng)生成橋接方法的情況。那么是否還有其他場(chǎng)景下,編譯器也會(huì)生成橋接方法呢?

筆者在最近的日常工作中,因業(yè)務(wù)需要,研究 Java 字節(jié)碼層面的知識(shí)。具體是,需要根據(jù)類(lèi)字節(jié)碼,獲取特定方法名的方法入?yún)ⅲ朔椒谠创a中只有一個(gè)。但是在實(shí)際使用中發(fā)現(xiàn):在類(lèi)實(shí)現(xiàn)泛型接口的情況下,在字節(jié)碼層面,類(lèi)卻有兩個(gè)同名方法,導(dǎo)致無(wú)法確定哪個(gè)方法才是我們需要的方法。經(jīng)過(guò)研究發(fā)現(xiàn),其中一個(gè)方法是編譯器在編譯的過(guò)程中,自動(dòng)生成的橋接方法(bridge method),兩個(gè)方法可通過(guò)特定標(biāo)識(shí)區(qū)分。

注:此處的橋接方法,跟設(shè)計(jì)模式中的橋接模式,不是一個(gè)概念。

問(wèn)題描述

為了能夠說(shuō)明問(wèn)題,筆者模糊了實(shí)際業(yè)務(wù)場(chǎng)景的具體案例,用一個(gè)稍微簡(jiǎn)單,能夠說(shuō)明問(wèn)題的示例,來(lái)分析編譯器自動(dòng)生成的橋接方法(bridge method)。

我們知道,Java 泛型是JDK 5 中引入的一個(gè)新特性,應(yīng)用廣泛。比如,我們有一個(gè)操作算子泛型接口 Operator<T>,接口中有一個(gè) process(T t) 方法,其作用是對(duì)入?yún)?T 進(jìn)行邏輯處理。示例代碼如下:

/**
* @author renzhiqiang
* @date 2022/2/20 18:30
*/
public interface Operator<T> {
/**
* process method
* @param t
*/
void process(T t);
}

在實(shí)際業(yè)務(wù)場(chǎng)景中,我們會(huì)有不同的操作算子,實(shí)現(xiàn)Operator 接口,進(jìn)行業(yè)務(wù)邏輯處理。那么我們來(lái)創(chuàng)建一個(gè)具體的算子,并實(shí)現(xiàn)Operator 接口,重寫(xiě) process(T t) 方法。如下:

/**
* 用戶信息算子
* @author renzhiqiang
* @date 2022/2/20 18:30
*/
public class UserInfoOperator implements Operator<String> {
@Override
public void process(String s) {
// do something
}
}

其中,泛型接口中的入?yún)㈩?lèi)型 T,在實(shí)現(xiàn)類(lèi)中替換成了實(shí)際需要的類(lèi)型 java.lang.String。到這里,我們就準(zhǔn)備好了代碼樣例。

那么,我們的目標(biāo)是什么呢?就是要獲取UserInfoOperator#process(String s) 方法的參數(shù)類(lèi)型java.lang.String。讀到這里,讀者可能會(huì)想:這不很簡(jiǎn)單么,通過(guò)反射,根據(jù)Class#getDeclaredMethods(),獲取到 UserInfoOperator 的所有方法,再找到方法名是 process 的方法,然后再獲取到參數(shù)列表,不就可以獲取參數(shù)類(lèi)型java.lang.String 了么。

如果正在閱讀文章的你也這么想的話,那請(qǐng)繼續(xù)往下看。

根據(jù) Java 反射方法Class#getDeclaredMethods() 的描述:

Returns an array of Method objectsincluding public, protected, default (package) access, and private methods, butexcludes inherited methods.

翻譯過(guò)來(lái)就是:返回方法對(duì)象數(shù)組,包括公共方法、受保護(hù)方法、默認(rèn)(包)訪問(wèn)方法和私有方法,但不包括繼承方法。

根據(jù)我們的示例,如果我們通過(guò)反射,利用Class#getDeclaredMethods() 方法,我們預(yù)期的返回方法數(shù)組中,應(yīng)該只有一個(gè)方法名是process 才對(duì),但是這里卻有兩個(gè) process 方法。驚不驚奇,意不意外!

圖 debug 發(fā)現(xiàn) UserInfoOperator 類(lèi)的兩個(gè) process 方法

產(chǎn)生原因

編譯器生成 bridge 方法

我們知道,Java 源碼需要經(jīng)過(guò)編譯器編譯,生成對(duì)應(yīng)的 .class 文件,才能給 JVM 使用。在源碼中,我們只定義了一個(gè)名為 process 的方法。那么我們考慮,編譯器在編譯源碼的過(guò)程中,是否會(huì)進(jìn)行一些特的處理。為了更加直觀的查看編譯后的字節(jié)碼文件,在 Idea 安裝 jclasslib 插件,通過(guò) jclasslib 查看 UserInfoOperator 和 Operator 的字節(jié)碼。如下:

圖 jclasslib 查看 UserInfoOperator 類(lèi)的字節(jié)碼(第一個(gè) process 方法)

圖 jclasslib 查看 UserInfoOperator 類(lèi)的字節(jié)碼 (第二個(gè) process 方法)

圖 jclasslib 查看 Operator 類(lèi)的字節(jié)碼

通過(guò) jclasslib 查看 .class 文件發(fā)現(xiàn),在 UserInfoOperator 類(lèi)中確實(shí)存在兩個(gè) process 方法:其中一個(gè)方法入?yún)⑹? java.lang.String,另一個(gè)方法的入?yún)⑹?java.lang.Object。而在 Operator 字節(jié)碼中,只有一個(gè) process 方法,方法的入?yún)⑹?java.lang.Object。同時(shí)我們注意到,在 UserInfoOperator 類(lèi)的字節(jié)碼中, [訪問(wèn)標(biāo)志]項(xiàng),其中一個(gè)方法的訪問(wèn)標(biāo)志是 [public synthetic bridge]。其中 public 很好理解,但是其中的 [synthetic bridge] 是怎么來(lái)的呢?

查閱相關(guān)資料后發(fā)現(xiàn),標(biāo)識(shí)符 synthetic ,表示此方法是否是由編譯器自動(dòng)產(chǎn)生的;標(biāo)識(shí)符 bridge,表示此方法是否是由編譯器產(chǎn)生的橋接方法。

圖 方法訪問(wèn)標(biāo)志(來(lái)源:深入理解 Java 虛擬機(jī)(第三版))

到此,可以確定的是,其中一個(gè)process 方法,是編譯器自動(dòng)產(chǎn)生的橋接方法。那么為什么編譯器會(huì)產(chǎn)生橋接方法呢?以及在什么情況下,會(huì)產(chǎn)生橋接方法?以及如何判斷一個(gè)方法是不是橋接方法?我們繼續(xù)往下分析。

為何生成 bridge 方法

正確編譯

在源碼中,Operator 類(lèi)的 process 方法的參數(shù)定義是 process(T t),參數(shù)類(lèi)型是 T。而在字節(jié)碼層面我們看到,process 方法在編譯之后,編譯器將入?yún)㈩?lèi)型變成了 java.lang.Object。偽代碼示意,大概是這樣:

public interface Operator<Object> {
/**
* 方法參數(shù)變成 Object 類(lèi)型
* @param object
*/
void process(Object object);
}

想象一下,如果沒(méi)有編譯器自動(dòng)生成的橋接方法,那么在編譯層面是不能通過(guò)的:因?yàn)榻涌?Operator 中的 process 方法,,經(jīng)過(guò)編譯之后,參數(shù)類(lèi)型變成了 java.lang.Object 類(lèi)型,而實(shí)現(xiàn)類(lèi) UserInfoOperator 中的 process 方法的參數(shù)是 java.lang.String 類(lèi)型,兩者的方法參數(shù)不一致,導(dǎo)致UserInfoOperator 并沒(méi)有重寫(xiě)接口中的 process 方法,因此編譯無(wú)法通過(guò)。

這種情況下,編譯器自動(dòng)生成一個(gè)橋接方法 void process(Object obj) 方法,則可以編譯通過(guò),似乎是理所當(dāng)然的事情。自動(dòng)生成的 process方法,方法簽名為:void process(Object object)。偽代碼示意,大概是這樣:

// 自動(dòng)生成的process 方法
public void process(Object object) {
process((String) object);
}

類(lèi)型擦除

我們知道,Java 中的泛型在編譯期間會(huì)將泛型信息擦除。如代碼定義 List 和 List,編譯之后都會(huì)變成 List。我們?cè)倏紤]一種常見(jiàn)的情形:Java 類(lèi)庫(kù)中比較器的用法。我們自定義比較器的時(shí)候,可以通過(guò)實(shí)現(xiàn) Comparator 接口,實(shí)現(xiàn)比較邏輯。示例代碼如下:

public class MyComparator implements Comparator<Integer> {
public int compare(Integer a,Integer b) {
// 比較邏輯
}
}

這種情況下,編譯器同樣會(huì)產(chǎn)生一個(gè)橋接方法。方法簽名為 intcompare(Object a, Object b) 。

圖 MyComparator 類(lèi)的兩個(gè) compare 方法

偽代碼示意,大概是這樣:

public class MyComparator implements Comparator<Integer> {
public int compare(Integer a,Integer b) {
// 比較邏輯
}
// 橋接方法 (bridge method)
public int compare(Object a,Object b) {
return compare((Integer)a,(Integer)b);
}
}

因此,當(dāng)我們使用如下方式進(jìn)行比較的時(shí)候,能夠通過(guò)編譯并得到我們預(yù)期的結(jié)果:

Object a = 5;
Object b = 6;
Comparator rawComp = new MyComparator();
// 可以通過(guò)編譯,因?yàn)樽詣?dòng)生成了橋接方法compare(Object a, Object b)
int comp = rawComp.compare(a, b);

另外,我們知道,泛型編譯之后,類(lèi)型信息會(huì)被擦除。如果我們有這樣一個(gè)比較方法:

// 比較方法
public <T> T max(List<T> list, Comparator<T> comparator){
T biggestSoFar = list.get(0);
for ( T t : list ) {
if (comparator.compare(t,biggestSoFar) > 0) {
biggestSoFar = t;
}
}
return biggestSoFar;
}

編譯之后,泛型被擦除掉,偽代碼表示,大概是這樣:

public Object max(List list, Comparator comparator) {
Object biggestSoFar =list.get(0);
for ( Object t : list ) {
if (comparator.compare(t,biggestSoFar) > 0) { //比較邏輯
biggestSoFar = t;
}
}
return biggestSoFar;
}

我們將 MyComparator 其中一個(gè)參數(shù)傳入 max() 方法。如果沒(méi)有橋接方法的話,那么第四行的比較邏輯,將無(wú)法正確編譯,因?yàn)镸yComparator 類(lèi)中沒(méi)有兩個(gè)參數(shù)是 Object 類(lèi)型的比較方法,只有參數(shù)類(lèi)型是 Integer 類(lèi)型的比較方法。讀者可自行測(cè)試。

解決方案

通過(guò)以上的案例描述,我們知道,在實(shí)現(xiàn)泛型接口的場(chǎng)景下,編譯器會(huì)自動(dòng)生成橋接方法,保證編譯能夠通過(guò)。那么在這種情況下,我們只要識(shí)別哪一個(gè)是橋接方法,哪一個(gè)不是橋接方法,就可以解決我們一開(kāi)始的問(wèn)題。很自然的,既然編譯器自動(dòng)產(chǎn)生了一個(gè)橋接方法,那么應(yīng)該會(huì)有某種方式,可以讓我們判斷一個(gè)方法是否是橋接方法。

果然,我們繼續(xù)研究發(fā)現(xiàn),Method 類(lèi)中提供了 Method#isBridge() 方法。查看源碼中對(duì)方法的描述:Method#isBridge():Returns true if this method is a bridge method;returns false otherwise。

到此,我們通過(guò)反射,獲取到 UserInfoOperator 類(lèi)中的兩個(gè)process 方法,再調(diào)用 Method#isBridge() 方法,即可鎖定需要的方法,因而進(jìn)一步獲取方法參數(shù) java.lang.String。

深入分析

至此可以說(shuō),就業(yè)務(wù)需求來(lái)說(shuō),我們完美的找到了解決方案。但在此之后,不禁會(huì)想:除了上述示例,還有哪些情況下,編譯器也會(huì)自動(dòng)生成橋接方法呢?我們繼續(xù)深入研究。

類(lèi)繼承

通過(guò)查閱相關(guān)資料,我們考慮如下一種情況:

/**
* 如下會(huì)產(chǎn)生橋接方法嗎?
* @author renzhiqiang
* @date 2022/2/20 18:33
*/
public class BridgeMethodSample {
static class A {
public void foo() {
}
}
public static class C extends A{
}
public static class D extends A{
@Override
public void foo() {
}
}
}

上述代碼示例中,我們定義了三個(gè)靜態(tài)內(nèi)部類(lèi):A C D,其中 C D 分別繼承 A。經(jīng)過(guò)編譯,通過(guò)jclasslib 查看 BridgeMethodSample 字節(jié)碼,我們也發(fā)現(xiàn):類(lèi) C 中編譯器為其生成了橋接方法 void foo(),而類(lèi) D 中卻沒(méi)有。

圖 類(lèi)C 生成橋接方法

圖 類(lèi)D 沒(méi)有生成橋接方法

深入分析,并根據(jù)上述分析的經(jīng)驗(yàn),我們猜測(cè),編譯器生成橋接方法,一定是在某種情況下需要一個(gè)方法,來(lái)滿足 Java 編程規(guī)范,或者需要保證程序運(yùn)行的正確性。通過(guò)字節(jié)碼可以看出,類(lèi) A 沒(méi)有 public 修飾,包范圍以外的程序是沒(méi)有訪問(wèn)類(lèi) A 的權(quán)限的,更不用說(shuō)類(lèi) A 中的方法。

但是類(lèi) C 是有public 修飾,C 類(lèi)中的方法,包括繼承來(lái)的方法,是可以被包外的程序訪問(wèn)的。因此,編譯器需要生成一個(gè)橋接方法,以保證能夠訪問(wèn) foo() 方法,滿足程序的正確運(yùn)行。但是,類(lèi) D 同樣繼承 A,卻沒(méi)有生成橋接方法,根本原因是類(lèi) D 中重寫(xiě)了父類(lèi) A 中的 foo() 方法,即沒(méi)有必要生成橋接方法。

方法重寫(xiě)

我們?cè)倏匆环N情況,方法重寫(xiě)。

Java 中,方法重寫(xiě)(Override),是子類(lèi)對(duì)父類(lèi)的允許訪問(wèn)的方法的實(shí)現(xiàn)過(guò)程進(jìn)行重新編寫(xiě)的過(guò)程。重寫(xiě)需要滿足一定的規(guī)則:

1. The method must have the same name as in the parentclass.

2. The method must have the same parameter as in theparent class.

3. There must be an IS-A relationship (inheritance).

JDK 5 之后,重寫(xiě)方法的返回類(lèi)型,可以與父類(lèi)方法返回類(lèi)型相同,也可以不相同,但必須是父類(lèi)方法返回類(lèi)型的子類(lèi)。我們考慮如下代碼示例:

// 定義一個(gè)父類(lèi),包含一個(gè) test() 方法
public class Father {
public Object test(String s) {
return s;
}
}
// 定義一個(gè)子類(lèi),繼承父類(lèi)
public class Child extends Father {
@Override
public String test(String s) {
return s;
}
}

以上,在 Child 子類(lèi)中,我們重寫(xiě)了 test() 方法,但是返回值的類(lèi)型,我們將 java.lang.Object 改變?yōu)樗淖宇?lèi) java.lang.String。編譯之后,我們同樣使用 jclasslib 插件,查看兩個(gè)類(lèi)的字節(jié)碼,如下所示:

圖 Child 類(lèi)字節(jié)碼test() 方法(1)

圖 Child 類(lèi)字節(jié)碼test() 方法(2)

圖 Father類(lèi)字節(jié)碼test() 方法

根據(jù)上圖我們發(fā)現(xiàn),Child 類(lèi)中我們重寫(xiě)了 test() 方法,但是在字節(jié)碼層面,發(fā)現(xiàn)有兩個(gè) test() 方法,其中一個(gè)方法的訪問(wèn)標(biāo)志為 [public synthetic bridge], 表示這個(gè)方法是編譯器為我們生成的。而當(dāng)我們不改變 Child#test() 方法的返回類(lèi)型時(shí),編譯器并沒(méi)有為我們生成橋接方法,讀者可自行試驗(yàn)。

也就是說(shuō),在子類(lèi)方法重寫(xiě)父類(lèi)方法,返回類(lèi)型不一致的情況下,編譯器也為我們生成了橋接方法。

以上,筆者羅列了幾種編譯器為我們自動(dòng)生成橋接方法的情況。那么是否還有其他場(chǎng)景下,編譯器也會(huì)生成橋接方法呢?如果您也曾研究過(guò)或者使用過(guò) bridge 方法,歡迎交流討論。

同時(shí),給出一個(gè) bridge 方法的非官方定義,希望能夠給讀者一些啟發(fā):

Bridge Method: These are methods that create an intermediate layerbetween the source and the target functions. It is usually used as part of thetype erasure process. It means that the bridge method is required as a typesafe interface.

限于筆者水平有限,難免有理解不準(zhǔn)確、不到位的地方。歡迎交流討論!

參考

https://stackoverflow.com/questions/5007357/java-generics-bridge-method

https://stackoverflow.com/questions/14144888/find-generic-method-with-actual-types-from-getdeclaredmethods

https://www.geeksforgeeks.org/method-class-isbridge-method-in-java/


責(zé)任編輯:武曉燕 來(lái)源: 程序員小灰
相關(guān)推薦

2021-10-22 09:41:26

橋接模式設(shè)計(jì)

2010-03-19 11:31:15

點(diǎn)對(duì)多點(diǎn)無(wú)線橋接

2009-12-04 17:39:56

無(wú)線路由器中繼功能

2010-04-09 16:15:02

無(wú)線橋接設(shè)置

2020-10-28 11:56:47

橋接模式

2022-01-14 09:22:22

設(shè)計(jì)模式橋接

2023-03-30 11:26:37

5GWi-Fi

2009-08-18 11:12:34

Ubuntu下Virt橋接網(wǎng)絡(luò)配置

2009-05-13 09:39:00

雙網(wǎng)卡共享帶寬

2021-03-05 07:57:41

設(shè)計(jì)模式橋接

2022-05-11 08:17:15

Java字符串API

2011-06-01 16:10:42

JNBridge

2010-03-19 11:46:35

點(diǎn)對(duì)多點(diǎn)無(wú)線橋接

2019-07-30 08:28:44

VirtualBox橋接網(wǎng)絡(luò)

2010-03-19 11:07:57

點(diǎn)對(duì)點(diǎn)無(wú)線橋接模式

2010-04-08 17:02:14

無(wú)線橋接方案

2011-11-10 16:43:59

WDS無(wú)線橋接

2009-05-14 15:36:00

DHCP地址故障

2016-01-28 13:28:43

VoLTE科普4G

2009-12-01 10:24:45

路由器IP設(shè)置
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)