<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
之所以寫這麼一篇文章是因為在Spring中,經常會出現下面這種程式碼
// 判斷是否是橋接方法,如果是的話就返回這個方法 BridgeMethodResolver.findBridgedMethod(specificMethod);
這些程式碼對我之前也造成了不小疑惑,在徹底弄懂後通過本文分享出來,也能減少大家在閱讀程式碼過程中的障礙!
第一種情況:方法重寫的時候子父類別方法返回值不一致導致
public class Parent { public Number get(Number number){ System.out.println("parent's method invoke"); return 1; } } public class Son extends Parent { // 這裡對父類別的方法進行了重寫,但是返回值型別跟父類別中不一樣,父類別中的返回值型別為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) + "個方法名稱:" + methodName + ",方法返回值型別:" + returnType + " "); System.out.print(bridge ? " 是橋接方法" : " 不是橋接方法"); System.out.println(" 這個方法是在"+declaringClass.getSimpleName()+"上申明的"); } } } // 程式列印如下: 第1個方法名稱:get,方法返回值型別:class java.lang.Integer 不是橋接方法 這個方法是在Son上申明的 第2個方法名稱:get,方法返回值型別:class java.lang.Number 是橋接方法 這個方法是在Son上申明的
可以看到在上面的例子中Son類中就出現了橋接方法。
看到上面的程式碼的執行結果,大家肯定會有這麼兩個疑問
有這些疑問沒關係,我們帶著疑問往下看。
如果你認真看了上面的程式碼,你應該就會知道上面例子的特殊之處在於:
子類對父類別的方法進行了重寫,並且子類方法中的返回值型別跟父類別方法的返回值型別不一樣!!!!
那麼到底是不是這個原因導致的呢?我們不妨將上面例子中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; } } // 執行結果 第1個方法名稱:get,方法返回值型別:class java.lang.Number 不是橋接方法 這個方法是在Son上申明的
再次執行程式碼,會發現,橋接方法不見了,也只能看到一個方法。
那麼到現在我們就基本能確定了是因為重寫的時候子父類別方法返回值不一致導致出現了橋接方法。
第二種情況:子類重寫了父類別中帶有泛型的方法
參考連結:https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html
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) + "個方法名稱:" + methodName + ",引數型別:" + Arrays.toString(parameterTypes) + " "); System.out.print(bridge ? " 是橋接方法" : " 不是橋接方法"); System.out.println(" 這個方法是在" + declaringClass.getSimpleName() + "上申明的"); } } } // 執行結果: 第1個方法名稱:setData,引數型別:[class java.lang.Integer] 不是橋接方法 這個方法是在MyNode上申明的 第2個方法名稱:setData,引數型別:[class java.lang.Object] 是橋接方法 這個方法是在MyNode上申明的
看完上面的程式碼可能你的問題又來了
這些問題基本跟第一種情況的問題一樣,所以不要急,我們還是往下看
上面例子的特殊之處在於,子類重寫父類別中帶有泛型引數的方法。實際上子類重寫父類別帶有泛型返回值的方法也會出現上面這種情況,比如,我們將程式碼改成這樣
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; } // 新增一個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); } // 子類對新增的那個方法進行復寫 @Override public Integer getData() { System.out.println("MyNode.getData"); return super.getData(); } } // 程式執行結果 第1個方法名稱:setData,引數型別:[class java.lang.Object] 是橋接方法 這個方法是在MyNode上申明的 第2個方法名稱:setData,引數型別:[class java.lang.Integer] 不是橋接方法 這個方法是在MyNode上申明的 第3個方法名稱:getData,引數型別:[] 是橋接方法 這個方法是在MyNode上申明的 第4個方法名稱:getData,引數型別:[] 不是橋接方法 這個方法是在MyNode上申明的
可以發現,又出現了一個橋接方法。
接下來回牽涉到一些
JVM
的知識,希望大家能耐心看完哦。我一直認為最好的學習方式是帶著問題去學習,但是在這個過程中你可能又會碰到新的問題,那麼怎麼辦呢?
堅持,就是最好的辦法,再難的事情不過也就是打怪升級!
在上面我們探究什麼時候會出現橋接方法時,應該能感覺到,橋接方法的出現都是要滿足下面兩個條件才會出現
當滿足了上面兩個條件時,編譯器會自動為我生成橋接方法,因為編譯的後檔案是交由JVM
執行的,生成的這個橋接方法肯定就是為了JVM
進行方法呼叫時服務的,我們不妨大膽猜測,在這種情況下,是因為JVM在進行方法呼叫時,沒有辦法滿足我們的執行時多型,所以生成了橋接方法。要弄清楚這個問題,我們還是要從JVM
的方法呼叫說起。
JVM是怎麼呼叫方法的?
我們應該知道,JVM
要執行一個方法時必定需要先找到那個方法,對計算機而言,就是要定位到方法所在的記憶體地址。
那麼JVM
是如何定位到方法所在記憶體呢?
我們知道JVM
所執行的是class
檔案,我們的.java
檔案會經過編譯生成class
檔案後才能被JVM
執行。
如圖所示:
因為目前我們關注的是方法的呼叫,所以對class檔案的具體結構我們就不做過多分析了,我們主要就看看常數池跟方法表。
常數池
常數池中主要儲存下面三類資訊
方法表
對於常數池跟方法表我們不做過多介紹,這兩個隨便一個拿出來都能寫一篇文章,對於閱讀本文而言,你只需要知道它們儲存了上面的這些資訊即可。如果大家感興趣的話,推薦閱讀周志明老師的《深入理解Java虛擬機器器》
位元組碼分析
接下來我們就通過一段位元組碼的分析來看看JVM
到底是如何呼叫方法的,這裡就以我們前文中第一個例子中的程式碼來進行分析。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檔案執行javap -v -c
指令,得到如下位元組碼
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[]); // 方法的描述符,括號中的是引數,[Ljava/lang/String代表引數是一個String陣列,V是返回值,代表void descriptor: ([Ljava/lang/String;)V // 方法的標誌,public,static flags: ACC_PUBLIC, ACC_STATIC // 方法執行程式碼對應的位元組碼 Code: // 運算元棧深為2,本地變數表中有2兩個元素,引數個數為1 stack=2, locals=2, args_size=1 // 前三行指定對應的程式碼就是Parent person = new Son() // new指定,建立一個物件,並返回這個物件的參照 0: new #2 // class com/dmz/spring/java/Son // dup指令,將new指令返回的參照進行備份,一個賦值給區域性變數表中的值,另外一個用於執行invokespecial指令 3: dup // 進行初始化 4: invokespecial #3 // Method com/dmz/spring/java/Son."<init>":()V // 將建立出來的物件的參照儲存到區域性變數表中下標為1也就是第二個元素中,第一個元素儲存的是main方法的引數 7: astore_1 // 將參照壓入到運算元棧中,此時棧頂儲存的是一個指向son型別物件的參照 8: aload_1 // 常數1壓入運算元棧 9: iconst_1 // 執行常數池中 #4所對應的方法,也就是java/lang/Integer.valueOf方法 10: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; // 真正呼叫get方法的指令 13: invokevirtual #5 // Method com/dmz/spring/java/Parent.get:(Ljava/lang/Number;)Ljava/lang/Number; // 彈出運算元棧頂的值 16: pop 17: return // 程式碼行數跟指令的對應關係,比如在我的idea中,第10行程式碼對應的就是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"
接下來,我們使用圖解的方式來對上面的位元組碼做進一步的分析
接下來就要執行invokevirtual
指令,在執行這個指令我們將運算元棧的狀態放大來看看
棧頂儲存的是1,也就是執行對應方法的引數,棧底儲存的是執行Parent person = new Son()
得到的一個參照。
在上面的位元組碼中,我們發現invokevirtual
指令後面跟了一個#5
,這代表它參照了常數池中的第五號常數,對應的就是這個方法參照:
com/dmz/spring/java/Parent.get:(Ljava/lang/Number;)Ljava/lang/Number;
上面整個表示式代表了方法的簽名,com/dmz/spring/java/Parent
代表了方法所在類名,get
代表方法名,(Ljava/lang/Number;)
代表方法執行引數,Ljava/lang/Number
代表方法返回值。
根據運算元棧的資訊以及invokevirtual
所參照的方法簽名資訊,我們不難得出這條指令要去執行person
參照所指向的物件中的一個方法名為get
,方法引數為Number
,返回值為Number
的方法,但是請注意,我們的Son物件中沒有這樣的一個方法,我們在Son中重寫的方法是這樣的
public Integer get(Number number) { return 2; }
其返回值型別是Integer
,可能有的同學會有疑問,Integer
不是Number
的子類嗎?為什麼不能識別呢?
嗯,我也沒辦法回答這個問題,JVM
在對方法覆蓋的定義就是這樣,必須要方法簽名相同。
但是Java對於重寫的定義呢?只是要求方法的返回值型別相同就行了,正是因為這二者的差異,導致了編譯器不得不生成一個橋接方法來進行平衡。
那麼到底是不是這樣呢?我們不妨再來看看生成橋接方法的類的位元組碼,也就是Son.class
的位元組碼,對應如下(只放關鍵的部分了,實在太佔篇幅了):
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; // 看到這個ACC_BRIDGE的標記了嗎,代表它就是橋接方法 // 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 // 這一步看到了嗎?呼叫了那個被橋接的方法,也就是我們真正定義的重寫的方法 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;
到這裡你明白了嗎?橋接方法到底橋接的什麼?其實就是編譯器對JVM
到JAVA的一個橋接,編譯器為了滿足JAVA的重寫的語意,生成了一個方法描述符與父類別一致的方法,然後又呼叫了真實的我們定義的邏輯。這樣既滿足了JAVA重寫的要求,也符合了JVM
的規範。
以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45