Spring雜談 | 從橋接方法到JVM方法調用

前言

之因此寫這麼一篇文章是由於在Spring中,常常會出現下面這種代碼html

// 判斷是不是橋接方法,若是是的話就返回這個方法
BridgeMethodResolver.findBridgedMethod(specificMethod); 複製代碼

這些代碼對我以前也形成了不小疑惑,在完全弄懂後經過本文分享出來,也能減小你們在閱讀代碼過程當中的障礙!java

橋接方法

何時會出現橋接方法?

第一種狀況:方法重寫的時候子父類方法返回值不一致致使

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類中就出現了橋接方法程序員

看到上面的代碼的執行結果,你們確定會有這麼兩個疑問web

  1. 爲何再Son中會有兩個get方法?明明實際申明的只有一個啊
  2. 爲何其中一個方法仍是橋接方法呢?這個橋接到底橋接的是什麼?
  3. 它的返回值爲何跟父類中被複寫的參數類型同樣,也是Number類型?

有這些疑問不要緊,咱們帶着疑問往下看。spring

若是你認真看了上面的代碼,你應該就會知道上面例子的特殊之處在於:數組

子類對父類的方法進行了重寫,而且子類方法中的返回值類型跟父類方法的返回值類型不同!!!!微信

那麼究竟是不是這個緣由致使的呢?咱們不妨將上面例子中Son類的代碼更改以下:oracle

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上申明的 複製代碼

再次運行代碼,會發現,橋接方法不見了,也只能看到一個方法。編輯器

那麼到如今咱們就基本能肯定了是由於重寫的時候子父類方法返回值不一致致使出現了橋接方法。ide

第二種狀況:子類重寫了父類中帶有泛型的方法

參考連接: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) + "個方法名稱:" + 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上申明的 複製代碼

看完上面的代碼可能你的問題又來了

  1. 爲何再MyNode中會有兩個setData方法?明明實際申明的只有一個啊
  2. 爲何其中一個方法仍是橋接方法呢?這個橋接到底橋接的是什麼?
  3. 它的參數類型爲何跟父類中被複寫的方法的參數類型同樣,也是Integer類型?

這些問題基本跟第一種狀況的問題同樣,因此不要急,咱們仍是往下看

上面例子的特殊之處在於,子類重寫父類中帶有泛型參數的方法。實際上子類重寫父類帶有泛型返回值的方法也會出現上面這種狀況,好比,咱們將代碼改爲這樣

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的知識,但願你們能耐心看完哦。

我一直認爲最好的學習方式是帶着問題去學習,可是在這個過程當中你可能又會碰到新的問題,那麼怎麼辦呢?

堅持,就是最好的辦法,再難的事情不過也就是打怪升級!

在上面咱們探究何時會出現橋接方法時,應該能感受到,橋接方法的出現都是要知足下面兩個條件纔會出現

  1. 子類重寫了父類的方法
  2. 子類中進行重寫的方法跟父類不一致(參數不一致或者返回值不一致)

當知足了上面兩個條件時,編譯器會自動爲我生成橋接方法,由於編譯的後文件是交由JVM執行的,生成的這個橋接方法確定就是爲了JVM進行方法調用時服務的,咱們不妨大膽猜想,在這種狀況下,是由於JVM在進行方法調用時,沒有辦法知足咱們的運行時多態,因此生成了橋接方法。要弄清楚這個問題,咱們仍是要從JVM的方法調用提及。

JVM是怎麼調用方法的?

咱們應該知道,JVM要執行一個方法時一定須要先找到那個方法,對計算機而言,就是要定位到方法所在的內存地址。那麼JVM是如何定位到方法所在內存呢?咱們知道JVM所執行的是class文件,咱們的.java文件會通過編譯生成class文件後才能被JVM執行。如圖所示:

未命名文件
未命名文件

由於目前咱們關注的是方法的調用,因此對class文件的具體結構咱們就不作過多分析了,咱們主要就看看常量池方法表

常量池

常量池中主要保存下面三類信息

  • 類和接口的全限定名
  • 字段的名稱和描述符
  • 方法的名稱和描述符

方法表

  • 方法標誌,好比public,native,abstract,以及本文所探討的橋接(bridge)
  • 方法名稱索引,由於具體的方法名稱保存在常量池中,因此這裏保存的是對常量池的索引
  • 描述符索引,即 返回值+參數
  • 屬性表集合,方法具體的執行代碼便保存在這裏

對於常量池跟方法表咱們不作過多介紹,這兩個隨便一個拿出來都能寫一篇文章,對於閱讀本文而言,你只須要知道它們保存了上面的這些信息便可。若是你們感興趣的話,推薦閱讀周志明老師的《深刻理解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"  複製代碼

接下來,咱們使用圖解的方式來對上面的字節碼作進一步的分析

字節碼圖解1
字節碼圖解1
字節碼分析2
字節碼分析2
字節碼分析3
字節碼分析3

接下來就要執行invokevirtual指令,在執行這個指令咱們將操做數棧的狀態放大來看看

字節碼圖解4
字節碼圖解4

棧頂保存的是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的規範。

若是本文對你由幫助的話,記得點個贊吧!也歡迎關注個人公衆號,微信搜索:程序員DMZ,或者掃描下方二維碼,跟着我一塊兒認認真真學Java,踏踏實實作一個coder。

公衆號
公衆號

我叫DMZ,一個在學習路上匍匐前行的小菜鳥!

相關文章
相關標籤/搜索