Spring之從橋接方法到JVM方法調(diào)用解讀
前言
之所以寫這么一篇文章是因?yàn)樵赟pring中,經(jīng)常會出現(xiàn)下面這種代碼
// 判斷是否是橋接方法,如果是的話就返回這個(gè)方法 BridgeMethodResolver.findBridgedMethod(specificMethod);
這些代碼對我之前也造成了不小疑惑,在徹底弄懂后通過本文分享出來,也能減少大家在閱讀代碼過程中的障礙!
橋接方法
什么時(shí)候會出現(xiàn)橋接方法?
第一種情況:方法重寫的時(shí)候子父類方法返回值不一致導(dǎo)致
public class Parent { public Number get(Number number){ System.out.println("parent's method invoke"); return 1; } } public class Son extends Parent { // 這里對父類的方法進(jìn)行了重寫,但是返回值類型跟父類中不一樣,父類中的返回值類型為Number,子類中的返回值類型為Integer,Integer是Number的子類 @Override public Integer get(Number number) { System.out.println("son's method invoke"); return 2; } } public class PMain { public static void main(String[] args) { Son son = new Son(); Method[] declaredMethods = son.getClass().getDeclaredMethods(); for (int i = 0; i < declaredMethods.length; i++) { Method declaredMethod = declaredMethods[i]; String methodName = declaredMethod.getName(); Class<?> returnType = declaredMethod.getReturnType(); Class<?> declaringClass = declaredMethod.getDeclaringClass(); boolean bridge = declaredMethod.isBridge(); System.out.print("第" + (i+1) + "個(gè)方法名稱:" + methodName + ",方法返回值類型:" + returnType + " "); System.out.print(bridge ? " 是橋接方法" : " 不是橋接方法"); System.out.println(" 這個(gè)方法是在"+declaringClass.getSimpleName()+"上申明的"); } } } // 程序打印如下: 第1個(gè)方法名稱:get,方法返回值類型:class java.lang.Integer 不是橋接方法 這個(gè)方法是在Son上申明的 第2個(gè)方法名稱:get,方法返回值類型:class java.lang.Number 是橋接方法 這個(gè)方法是在Son上申明的
可以看到在上面的例子中Son類中就出現(xiàn)了橋接方法。
看到上面的代碼的執(zhí)行結(jié)果,大家肯定會有這么兩個(gè)疑問
- 為什么再Son中會有兩個(gè)get方法?明明實(shí)際申明的只有一個(gè)啊
- 為什么其中一個(gè)方法還是橋接方法呢?這個(gè)橋接到底橋接的是什么?
- 它的返回值為什么跟父類中被復(fù)寫的參數(shù)類型一樣,也是Number類型?
有這些疑問沒關(guān)系,我們帶著疑問往下看。
如果你認(rèn)真看了上面的代碼,你應(yīng)該就會知道上面例子的特殊之處在于:
子類對父類的方法進(jìn)行了重寫,并且子類方法中的返回值類型跟父類方法的返回值類型不一樣?。。?!
那么到底是不是這個(gè)原因?qū)е碌哪??我們不妨將上面例子中Son類的代碼更改如下:
public class Son extends Parent { // @Override // public Integer get(Number number) { // System.out.println("son's method invoke"); // return 2; // } @Override public Number get(Number number) { System.out.println("son's method invoke"); return 2; } } // 運(yùn)行結(jié)果 第1個(gè)方法名稱:get,方法返回值類型:class java.lang.Number 不是橋接方法 這個(gè)方法是在Son上申明的
再次運(yùn)行代碼,會發(fā)現(xiàn),橋接方法不見了,也只能看到一個(gè)方法。
那么到現(xiàn)在我們就基本能確定了是因?yàn)?strong>重寫的時(shí)候子父類方法返回值不一致導(dǎo)致出現(xiàn)了橋接方法。
第二種情況:子類重寫了父類中帶有泛型的方法
參考鏈接:https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html#bridgeMethods
public class Node<T> { public T data; public Node(T data) { this.data = data; } public void setData(T data) { System.out.println("Node.setData"); this.data = data; } } public class MyNode extends Node<Integer> { public MyNode(Integer data) { super(data); } @Override public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } } public class Main { public static void main(String[] args) { MyNode mn = new MyNode(5); Method[] declaredMethods = mn.getClass().getDeclaredMethods(); for (int i = 0; i < declaredMethods.length; i++) { Method declaredMethod = declaredMethods[i]; String methodName = declaredMethod.getName(); Class<?>[] parameterTypes = declaredMethod.getParameterTypes(); Class<?> declaringClass = declaredMethod.getDeclaringClass(); boolean bridge = declaredMethod.isBridge(); System.out.print("第" + (i + 1) + "個(gè)方法名稱:" + methodName + ",參數(shù)類型:" + Arrays.toString(parameterTypes) + " "); System.out.print(bridge ? " 是橋接方法" : " 不是橋接方法"); System.out.println(" 這個(gè)方法是在" + declaringClass.getSimpleName() + "上申明的"); } } } // 運(yùn)行結(jié)果: 第1個(gè)方法名稱:setData,參數(shù)類型:[class java.lang.Integer] 不是橋接方法 這個(gè)方法是在MyNode上申明的 第2個(gè)方法名稱:setData,參數(shù)類型:[class java.lang.Object] 是橋接方法 這個(gè)方法是在MyNode上申明的
看完上面的代碼可能你的問題又來了
- 為什么再M(fèi)yNode中會有兩個(gè)setData方法?明明實(shí)際申明的只有一個(gè)啊
- 為什么其中一個(gè)方法還是橋接方法呢?這個(gè)橋接到底橋接的是什么?
- 它的參數(shù)類型為什么跟父類中被復(fù)寫的方法的參數(shù)類型一樣,也是Integer類型?
這些問題基本跟第一種情況的問題一樣,所以不要急,我們還是往下看
上面例子的特殊之處在于,子類重寫父類中帶有泛型參數(shù)的方法。實(shí)際上子類重寫父類帶有泛型返回值的方法也會出現(xiàn)上面這種情況,比如,我們將代碼改成這樣
public class Node<T> { public T data; public Node(T data) { this.data = data; } public void setData(T data) { System.out.println("Node.setData"); this.data = data; } // 新增一個(gè)getData方法,返回值為泛型T public T getData() { System.out.println("Node.getData"); return this.data; } } public class MyNode extends Node<Integer> { public MyNode(Integer data) { super(data); } @Override public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } // 子類對新增的那個(gè)方法進(jìn)行復(fù)寫 @Override public Integer getData() { System.out.println("MyNode.getData"); return super.getData(); } } // 程序運(yùn)行結(jié)果 第1個(gè)方法名稱:setData,參數(shù)類型:[class java.lang.Object] 是橋接方法 這個(gè)方法是在MyNode上申明的 第2個(gè)方法名稱:setData,參數(shù)類型:[class java.lang.Integer] 不是橋接方法 這個(gè)方法是在MyNode上申明的 第3個(gè)方法名稱:getData,參數(shù)類型:[] 是橋接方法 這個(gè)方法是在MyNode上申明的 第4個(gè)方法名稱:getData,參數(shù)類型:[] 不是橋接方法 這個(gè)方法是在MyNode上申明的
可以發(fā)現(xiàn),又出現(xiàn)了一個(gè)橋接方法。
為什么需要橋接方法?
接下來回牽涉到一些
JVM
的知識,希望大家能耐心看完哦。我一直認(rèn)為最好的學(xué)習(xí)方式是帶著問題去學(xué)習(xí),但是在這個(gè)過程中你可能又會碰到新的問題,那么怎么辦呢?
堅(jiān)持,就是最好的辦法,再難的事情不過也就是打怪升級!
在上面我們探究什么時(shí)候會出現(xiàn)橋接方法時(shí),應(yīng)該能感覺到,橋接方法的出現(xiàn)都是要滿足下面兩個(gè)條件才會出現(xiàn)
- 子類重寫了父類的方法
- 子類中進(jìn)行重寫的方法跟父類不一致(參數(shù)不一致或者返回值不一致)
當(dāng)滿足了上面兩個(gè)條件時(shí),編譯器會自動為我生成橋接方法,因?yàn)榫幾g的后文件是交由JVM
執(zhí)行的,生成的這個(gè)橋接方法肯定就是為了JVM
進(jìn)行方法調(diào)用時(shí)服務(wù)的,我們不妨大膽猜測,在這種情況下,是因?yàn)镴VM在進(jìn)行方法調(diào)用時(shí),沒有辦法滿足我們的運(yùn)行時(shí)多態(tài),所以生成了橋接方法。要弄清楚這個(gè)問題,我們還是要從JVM
的方法調(diào)用說起。
JVM是怎么調(diào)用方法的?
我們應(yīng)該知道,JVM
要執(zhí)行一個(gè)方法時(shí)必定需要先找到那個(gè)方法,對計(jì)算機(jī)而言,就是要定位到方法所在的內(nèi)存地址。
那么JVM
是如何定位到方法所在內(nèi)存呢?
我們知道JVM
所執(zhí)行的是class
文件,我們的.java
文件會經(jīng)過編譯生成class
文件后才能被JVM
執(zhí)行。
如圖所示:
因?yàn)槟壳拔覀冴P(guān)注的是方法的調(diào)用,所以對class文件的具體結(jié)構(gòu)我們就不做過多分析了,我們主要就看看常量池跟方法表。
常量池
常量池中主要保存下面三類信息
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
方法表
- 方法標(biāo)志,比如public,native,abstract,以及本文所探討的橋接(bridge)
- 方法名稱索引,因?yàn)榫唧w的方法名稱保存在常量池中,所以這里保存的是對常量池的索引
- 描述符索引,即返回值+參數(shù)
- 屬性表集合,方法具體的執(zhí)行代碼便保存在這里
對于常量池跟方法表我們不做過多介紹,這兩個(gè)隨便一個(gè)拿出來都能寫一篇文章,對于閱讀本文而言,你只需要知道它們保存了上面的這些信息即可。如果大家感興趣的話,推薦閱讀周志明老師的《深入理解Java虛擬機(jī)》
字節(jié)碼分析
接下來我們就通過一段字節(jié)碼的分析來看看JVM
到底是如何調(diào)用方法的,這里就以我們前文中第一個(gè)例子中的代碼來進(jìn)行分析。java
代碼如下:
public class Parent { public Number get(Number number){ return 1; } } public class Son extends Parent { // 重寫了父類的方法,返回值類型只要是Number類的子類即可 @Override public Integer get(Number number) { return 2; } } /** * @author 程序員DMZ * @Date Create in 21:03 2020/6/7 * @Blog https://daimingzhi.blog.csdn.net/ */ public class LoadMain { public static void main(String[] args) { Parent person = new Son(); person.get(1); } }
對編譯好的class文件執(zhí)行javap -v -c
指令,得到如下字節(jié)碼
Classfile /E:/spring-framework/spring-dmz/out/production/classes/com/dmz/spring/java/LoadMain.class Last modified 2020-6-7; size 673 bytes MD5 checksum 4b8832849fb5f63e472324be91603b1b Compiled from "LoadMain.java" public class com.dmz.spring.java.LoadMain minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER // 常量池 Constant pool: #1 = Methodref #7.#23 // java/lang/Object."<init>":()V #2 = Class #24 // com/dmz/spring/java/Son #3 = Methodref #2.#23 // com/dmz/spring/java/Son."<init>":()V #4 = Methodref #25.#26 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #5 = Methodref #27.#28 // com/dmz/spring/java/Parent.get:(Ljava/lang/Number;)Ljava/lang/Number; #6 = Class #29 // com/dmz/spring/java/LoadMain #7 = Class #30 // java/lang/Object #8 = Utf8 <init> #9 = Utf8 ()V #10 = Utf8 Code #11 = Utf8 LineNumberTable #12 = Utf8 LocalVariableTable #13 = Utf8 this #14 = Utf8 Lcom/dmz/spring/java/LoadMain; #15 = Utf8 main #16 = Utf8 ([Ljava/lang/String;)V #17 = Utf8 args #18 = Utf8 [Ljava/lang/String; #19 = Utf8 person #20 = Utf8 Lcom/dmz/spring/java/Parent; #21 = Utf8 SourceFile #22 = Utf8 LoadMain.java #23 = NameAndType #8:#9 // "<init>":()V #24 = Utf8 com/dmz/spring/java/Son #25 = Class #31 // java/lang/Integer #26 = NameAndType #32:#33 // valueOf:(I)Ljava/lang/Integer; #27 = Class #34 // com/dmz/spring/java/Parent #28 = NameAndType #35:#36 // get:(Ljava/lang/Number;)Ljava/lang/Number; #29 = Utf8 com/dmz/spring/java/LoadMain #30 = Utf8 java/lang/Object #31 = Utf8 java/lang/Integer #32 = Utf8 valueOf #33 = Utf8 (I)Ljava/lang/Integer; #34 = Utf8 com/dmz/spring/java/Parent #35 = Utf8 get #36 = Utf8 (Ljava/lang/Number;)Ljava/lang/Number; { public com.dmz.spring.java.LoadMain(); descriptor: ()V 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 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/dmz/spring/java/LoadMain;guan public static void main(java.lang.String[]); // 方法的描述符,括號中的是參數(shù),[Ljava/lang/String代表參數(shù)是一個(gè)String數(shù)組,V是返回值,代表void descriptor: ([Ljava/lang/String;)V // 方法的標(biāo)志,public,static flags: ACC_PUBLIC, ACC_STATIC // 方法執(zhí)行代碼對應(yīng)的字節(jié)碼 Code: // 操作數(shù)棧深為2,本地變量表中有2兩個(gè)元素,參數(shù)個(gè)數(shù)為1 stack=2, locals=2, args_size=1 // 前三行指定對應(yīng)的代碼就是Parent person = new Son() // new指定,創(chuàng)建一個(gè)對象,并返回這個(gè)對象的引用 0: new #2 // class com/dmz/spring/java/Son // dup指令,將new指令返回的引用進(jìn)行備份,一個(gè)賦值給局部變量表中的值,另外一個(gè)用于執(zhí)行invokespecial指令 3: dup // 進(jìn)行初始化 4: invokespecial #3 // Method com/dmz/spring/java/Son."<init>":()V // 將創(chuàng)建出來的對象的引用存儲到局部變量表中下標(biāo)為1也就是第二個(gè)元素中,第一個(gè)元素存儲的是main方法的參數(shù) 7: astore_1 // 將引用壓入到操作數(shù)棧中,此時(shí)棧頂保存的是一個(gè)指向son類型對象的引用 8: aload_1 // 常數(shù)1壓入操作數(shù)棧 9: iconst_1 // 執(zhí)行常量池中 #4所對應(yīng)的方法,也就是java/lang/Integer.valueOf方法 10: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; // 真正調(diào)用get方法的指令 13: invokevirtual #5 // Method com/dmz/spring/java/Parent.get:(Ljava/lang/Number;)Ljava/lang/Number; // 彈出操作數(shù)棧頂?shù)闹? 16: pop 17: return // 代碼行數(shù)跟指令的對應(yīng)關(guān)系,比如在我的idea中,第10行代碼對應(yīng)的就是Parent person = new Son() LineNumberTable: line 10: 0 line 11: 8 line 12: 17 // 局部變量表中的值 LocalVariableTable: Start Length Slot Name Signature 0 18 0 args [Ljava/lang/String; 8 10 1 person Lcom/dmz/spring/java/Parent; } SourceFile: "LoadMain.java"
接下來,我們使用圖解的方式來對上面的字節(jié)碼做進(jìn)一步的分析
接下來就要執(zhí)行invokevirtual
指令,在執(zhí)行這個(gè)指令我們將操作數(shù)棧的狀態(tài)放大來看看
棧頂保存的是1,也就是執(zhí)行對應(yīng)方法的參數(shù),棧底保存的是執(zhí)行Parent person = new Son()
得到的一個(gè)引用。
在上面的字節(jié)碼中,我們發(fā)現(xiàn)invokevirtual
指令后面跟了一個(gè)#5
,這代表它引用了常量池中的第五號常量,對應(yīng)的就是這個(gè)方法引用:
com/dmz/spring/java/Parent.get:(Ljava/lang/Number;)Ljava/lang/Number;
上面整個(gè)表達(dá)式代表了方法的簽名,com/dmz/spring/java/Parent
代表了方法所在類名,get
代表方法名,(Ljava/lang/Number;)
代表方法執(zhí)行參數(shù),Ljava/lang/Number
代表方法返回值。
根據(jù)操作數(shù)棧的信息以及invokevirtual
所引用的方法簽名信息,我們不難得出這條指令要去執(zhí)行person
引用所指向的對象中的一個(gè)方法名為get
,方法參數(shù)為Number
,返回值為Number
的方法,但是請注意,我們的Son對象中沒有這樣的一個(gè)方法,我們在Son中重寫的方法是這樣的
public Integer get(Number number) { return 2; }
其返回值類型是Integer
,可能有的同學(xué)會有疑問,Integer
不是Number
的子類嗎?為什么不能識別呢?
嗯,我也沒辦法回答這個(gè)問題,JVM
在對方法覆蓋的定義就是這樣,必須要方法簽名相同。
但是Java對于重寫的定義呢?只是要求方法的返回值類型相同就行了,正是因?yàn)檫@二者的差異,導(dǎo)致了編譯器不得不生成一個(gè)橋接方法來進(jìn)行平衡。
那么到底是不是這樣呢?我們不妨再來看看生成橋接方法的類的字節(jié)碼,也就是Son.class
的字節(jié)碼,對應(yīng)如下(只放關(guān)鍵的部分了,實(shí)在太占篇幅了):
public java.lang.Integer get(java.lang.Number); descriptor: (Ljava/lang/Number;)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 13: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/dmz/spring/java/Son; 0 5 1 number Ljava/lang/Number; public java.lang.Number get(java.lang.Number); descriptor: (Ljava/lang/Number;)Ljava/lang/Number; // 看到這個(gè)ACC_BRIDGE的標(biāo)記了嗎,代表它就是橋接方法 // ACC_SYNTHETIC,代表是編譯器生成的,編譯器生成的方法不一定是橋接方法,但是橋接方法一定是編譯器生成的 // ACC_PUBLIC不用說了吧 flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: aload_1 // 這一步看到了嗎?調(diào)用了那個(gè)被橋接的方法,也就是我們真正定義的重寫的方法 2: invokevirtual #3 // Method get:(Ljava/lang/Number;)Ljava/lang/Integer; 5: areturn LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/dmz/spring/java/Son;
總結(jié)
到這里你明白了嗎?橋接方法到底橋接的什么?其實(shí)就是編譯器對JVM
到JAVA的一個(gè)橋接,編譯器為了滿足JAVA的重寫的語義,生成了一個(gè)方法描述符與父類一致的方法,然后又調(diào)用了真實(shí)的我們定義的邏輯。這樣既滿足了JAVA重寫的要求,也符合了JVM
的規(guī)范。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
springboot項(xiàng)目中的bootstrap.yml配置不生效的原因及解決(沒有自動提示)
新創(chuàng)建一個(gè) springboot項(xiàng)目,添加了 bootstrap.yml 文件,發(fā)現(xiàn)文件并沒有如預(yù)期變成綠色葉子,編寫的時(shí)候也沒有自動提示,啟動的時(shí)候,發(fā)現(xiàn)端口是8080,由此發(fā)現(xiàn)配置并沒有生效,所以本文給大家講解了springboot項(xiàng)目中的bootstrap.yml配置不生效的原因及解決2024-01-01java實(shí)現(xiàn)Object轉(zhuǎn)String的4種方法小結(jié)
這篇文章主要介紹了java實(shí)現(xiàn)Object轉(zhuǎn)String的4種方法小結(jié),具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09Java向數(shù)據(jù)庫插入中文出現(xiàn)亂碼解決方案
這篇文章主要介紹了Java向數(shù)據(jù)庫插入中文出現(xiàn)亂碼解決方案,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08