「Java 路線」| 方法調(diào)用的本質(zhì)(含重載與重寫區(qū)別)

點贊關(guān)注,不再迷路培己,你的支持對我意義重大胚泌!

?? Hi,我是丑丑零蓉。本文 「Java 路線」| 導(dǎo)讀 —— 他山之石敌蜂,可以攻玉 已收錄津肛,這里有 Android 進階成長路線筆記 & 博客,歡迎跟著彭丑丑一起成長秸脱。(聯(lián)系方式在 GitHub)

前言

  • 對于習(xí)慣使用面向?qū)ο箝_發(fā)的工程師們來說部蛇,重載 & 重寫 這兩個概念應(yīng)該不會陌生了。在中 / 低級別面試中巷查,也常常會考察面試者對它們的理解(隱約記得當(dāng)年在校招面試時遇到過)岛请;
  • 網(wǎng)上大多數(shù)資料 & 面經(jīng)對這兩個概念的闡述幢踏,多數(shù)僅停留在討論兩者在 表現(xiàn)上 的差異,讓讀者去被動地接受知識僚匆。在這篇文章里咧擂,我將更有深度地理解重載 & 重寫的原理,應(yīng)深入理解Java 虛擬機執(zhí)行引擎是如何進行方法調(diào)用的云芦。請點贊贸桶,你的點贊和關(guān)注真的對我非常重要皇筛!

首先,嘗試寫出以下程序的輸出:

public class Base {
    public static void funcStatic(String str){
        System.out.println("Base - funcStatic - String");
    }
    public static void funcStatic(Object obj){
        System.out.println("Base - funcStatic - Object");
    }
    public void func(String str){
        System.out.println("Base - func - String");
    }
    public void func(Object obj){
        System.out.println("Base - func - Object");
    }
}
public class Child extends Base {
    public static void funcStatic(String str){
        System.out.println("Child - funcStatic - String");
    }
    public static void funcStatic(Object obj){
        System.out.println("Child - funcStatic - Object");
    }
    @Override
    public void func(String str){
        System.out.println("Child - func - String");
    }
    @Override
    public void func(Object obj){
        System.out.println("Child - func - Object");
    }
}
public class Test{
    public static void main(String[] args){
        Object obj = new Object();
        Object str = new String();

        Base base = new Base();
        Base child1 = new Child();
        Child child2 = new Child();

        base.funcStatic(obj); // 正常編程中不應(yīng)該用實例去調(diào)用靜態(tài)方法
        child1.funcStatic(obj);
        child2.funcStatic(obj);

        base.func(str);
        child1.func(str);
        child2.func(str);
    }
}

程序輸出:

Base - funcStatic - Object
Base - funcStatic - Object
Child - funcStatic - Object

Base - func - Object
Child - func - Object
Child - func - Object

程序輸出是否與你的預(yù)期一致呢?遇到困難了嗎拄踪,相信這篇文章一定能幫到你...

延伸文章

目錄


1. 靜態(tài)類型 & 實際類型

每一個變量都有兩種類型:靜態(tài)類型(Static Type) & 實際類型(Actual Type)。例如下面代碼中忿薇,Base為變量base的靜態(tài)類型躏哩,Child為實際類型:

Base base = new Child();

兩者的具體區(qū)別如下:

  • 靜態(tài)類型:引用變量的類型扫尺,在編譯期確定,無法改變
  • 實際類型:實例對象的類型弊攘,在編譯期無法確定,需在運行期確定迈倍,可以改變

這里先談到這里啼染,后文會從字節(jié)碼的角度理解繼續(xù)討論兩個類型焕梅。


2. 方法調(diào)用的本質(zhì)

這一節(jié),我們來討論Java中方法調(diào)用的本質(zhì)斜棚。我們知道打肝,Java前端編譯的產(chǎn)物是字節(jié)碼挪捕,與C/C++不同级零,前端編譯過程中并沒有鏈接步驟,字節(jié)碼中所有的方法調(diào)用都是使用符號引用鉴嗤。舉個例子:

- 源碼:

public class Child extends Base {

    @Override
    void func() {
    }

    void test1(){
        func();
    }

    void test2(){
        super.func();
    }
}

- 字節(jié)碼(javap -c Child.class):

Compiled from "Child.java"
public class com.Child extends com.Base {
  // 構(gòu)造函數(shù)序调,默認調(diào)用父類構(gòu)造函數(shù)
  public com.Child();
    Code:
       0: aload_0
       1: invokespecial #1 // Method com/Base."<init>":()V
       4: return

  void func();
    Code:
       0: return

  void test1();
    Code:
       0: aload_0
       // invokevirtual 調(diào)用實例方法
       1: invokevirtual #2 // Method func:()V
       4: return

  void test2();
    Code:
       0: aload_0
       // invokespecial 調(diào)用靜態(tài)方法
       1: invokespecial #3 // Method com/Base.func:()V
       4: return
}

上面的字節(jié)碼中发绢,invokespecialinvokevirtual都是方法調(diào)用的字節(jié)碼指令边酒,具體細節(jié)下文會詳細解釋。后面的#1 #2 #3表示符號引用在常量池中的索引號坯认,根據(jù)這個索引號檢索常量表,可以查到最終表示的是一個字符串字面量陋气,例如func:()V荆隘,這個就是方法的符號引用椰拒。

為了方便理解字節(jié)碼,javap反編譯的字節(jié)碼已經(jīng)在注釋中提示了最終表示的值褒脯,例如Method func:()V缆毁。

符號引用(Symbolic References)是一個用來無歧義地標(biāo)識一個實體(例如方法/字段)的字符串脊框,在運行期它會翻譯為直接引用(Direct Reference)。對于方法來說沉御,就是方法的入口地址吠裆。

下圖描述了方法符號引用的基本格式:

方法的符號引用

這個符號引用包含了變量的靜態(tài)類型(如果是變量的靜態(tài)類型與本類相同试疙,不需要指明)抠蚣、簡單方法名以及描述符(參數(shù)順序、參數(shù)類型和方法返回值)缓屠。通過這個符號引用,Java虛擬機就可以翻譯出該方法的直接引用储耐。但是,同一個符號引用晦攒,運行時翻譯出來的直接引用可能是不同的得哆,為什么會這樣呢贩据?

  • 小結(jié):
    1. 方法調(diào)用的本質(zhì)是根據(jù)方法的符號引用確定方法的直接引用(入口地址)

3. 從符號引用到直接引用

為什么同一個符號引用,運行時翻譯出來的直接引用可能是不同的矾芙?這與使用的方法調(diào)用指令的處理過程有關(guān)近上,Java字節(jié)碼的方法調(diào)用指令一共有以下 5 種:

五種方法調(diào)用指令

其中壹无,根據(jù)調(diào)用方法的版本是否在編譯期可以確定斗锭,(注意:只是版本,而不是入口地址骚秦,入口地址只能在運行時確定)可以將方法調(diào)用劃分為靜態(tài)解析 & 動態(tài)分派兩種璧微。

# 誤區(qū)(重要)#

《深入理解Java虛擬機》中將方法調(diào)用分為解析前硫、靜態(tài)分派、動態(tài)分派三種阶剑,又根據(jù)宗量的數(shù)量引入了靜態(tài)多分派牧愁,動態(tài)單分派的概念外莲。這些概念事實上過于字典化,也很容易讓讀者誤認為靜態(tài)分派與動態(tài)分派是非此即彼的互斥關(guān)系磨确。事實上乏奥,一個方法可以同時重寫與重載 ,重載 & 重寫是方法調(diào)用的兩個階段恨诱,而不是兩個種類胡野。

下面痕鳍,我將介紹Java中方法選擇的三個步驟:

3.1 步驟1:生成符號引用(編譯時)

上一節(jié)我們提到過方法符號引用的基本格式笼呆,分為三個部分:

  • 變量的靜態(tài)類型:
    類的全限定名中將.替換為/诗赌,例如java.lang.Object對應(yīng)java/lang/Object
  • 簡單名稱:
    方法的名稱,例如Object#toString()的簡單名稱為:toString
  • 描述符:
    方法的參數(shù)列表和返回值洪碳,例如Object#toString()的描述符為()LJava/lang/String;

描述符的規(guī)則不是本文重點瞳腌,這里便不再贅述了镜雨,若不了解可閱讀延伸文章苇羡。這里我們用兩段程序驗證上述規(guī)則遏乔,這兩段程序中我們考慮了重載 & 重寫妇穴、靜態(tài) & 實例兩個維度的因素:

程序一(重載 & 重寫)

public class Base {
    public void func() {}
    public void func(int i){}
}

public class Child extends Base {
    @Override
    public void func() {}
    @Override
    public void func(int i){}
}

public class Test{
    public static void main(String[] args){
        Base base1 = new Base();
        Base child1 = new Child();
        Child child2 = new Child();

        base1.func();  // invokevirtual com.Base.func:():V
        child1.func(); // invokevirtual com.Base.func:():V
        child2.func(); // invokevirtual com.Child.func:():V

        base1.func(1);  // invokevirtual com.Base.func:(I):V
        child1.func(1); // invokevirtual com.Base.func:(I):V
        child2.func(1); // invokevirtual com.Child.func:(I):V
    }
}

可以看到疗我,符號引用中的類名確實是變量的靜態(tài)類型,而不是變量的實際類型溺健;方法名不用多說鞭缭,方法描述符則選擇重載方法中最合適的一個方法岭辣。這個例程很容易判斷重載方法選擇結(jié)果,具體選擇規(guī)則其實更為復(fù)雜偷遗。

程序二(靜態(tài) & 實例)

public class Base {
    public static void func() {}
    public void func(int i){}
}

public class Child extends Base {
    public static void func() {}
    @Override
    public void func(int i){}
}

public class Test{
    public static void main(String[] args){
        Base base1 = new Base();
        Base child1 = new Child();
        Child child2 = new Child();

        符號引用與程序一相同,僅指令不同

        base1.func();  // invokestatic com.Base.func:():V
        child1.func(); // invokestatic com.Base.func:():V
        child2.func(); // invokestatic com.Child.func:():V

        base1.func(1);  // invokevirtual com.Base.func:(I):V
        child1.func(1); // invokevirtual com.Base.func:(I):V
        child2.func(1); // invokevirtual com.Child.func:(I):V
    }
}

可以看到,static對符號引用沒有影響纪铺,僅影響使用的指令(靜態(tài)方法調(diào)用使用invokestatic)。而通過對象實例去調(diào)用靜態(tài)方法是javac的語法糖烹棉,編譯時會轉(zhuǎn)換為使用變量的靜態(tài)類型固化到符號引用中。

  • 小結(jié):
    1. 方法的符號引用在編譯期確定抠刺,并固化到字節(jié)碼中方法調(diào)用指令的參數(shù)中
    2. 是否有static修飾對符號引用沒有影響速妖,僅影響使用的字節(jié)碼指令罕容,對象實例去調(diào)用靜態(tài)方法是javac的語法糖

3.2 步驟二:解析(類加載時)

為什么靜態(tài)方法锦秒、私有實例方法喉镰、實例構(gòu)造器<init>侣姆、父類方法以及final修飾這五種方法(對應(yīng)的關(guān)鍵字: static、private汇歹、<init>产弹、super弯囊、final)可以在編譯期確定版本呢匾嘱?因為無論運行時加載多少個類,這些方法都保證唯一的版本:

方法 原因
static 相同簽名的子類方法會隱藏父類方法
private 只在本類可見
<init> 由編譯器生成撬讽,源碼無法編寫
super Java是單繼承游昼,只有一個父類
final 禁止被重寫

既然可以確定方法的版本烘豌,虛擬機在處理invokestatic看彼、invokespecialinvokevirtual(final)時顽铸,就可以提前將符號引用轉(zhuǎn)換為直接引用鸯绿,不必延遲到方法調(diào)用時確定瓶蝴,具體來說舷手,是在類加載的解析階段完成轉(zhuǎn)換的劲绪。

invokestatic 指令
  • 1)類加載解析階段:根據(jù)符號引用中類名(如下例中java/lang/String變量的靜態(tài)類型中)贾富,在對應(yīng)的類中找到簡單名稱與描述符相符合的方法,如果找到則將符號引用轉(zhuǎn)換為直接引用汗捡;否則扇住,按照繼承關(guān)系從下往上依次在各個父類中搜索

  • 2)調(diào)用階段:符號引用已經(jīng)轉(zhuǎn)換為直接引用艘蹋;調(diào)用invokestatic不需要將對象加載到操作數(shù)棧票灰,只需要將所需要的參數(shù)入棧就可以執(zhí)行invokestatic指令屑迂。例如:

源碼:
String str = String.valueOf("1")

字節(jié)碼:
0: iconst_1
1: invokestatic  #2 // Method java/lang/String.valueOf:(I)Ljava/lang/String;
4: astore_1
invokespecial 指令
  • 1)類加載解析階段:同invokestatic屈糊,也是從符號引用中的靜態(tài)類型開始查找

  • 2)調(diào)用階段:同invokestatic逻锐,符號引用已經(jīng)轉(zhuǎn)換為直接引用雕薪;<init>所袁、父類方法凶掰、私有實例方法這3種情況都是屬于實例方法懦窘,所以調(diào)用invokespecial指令需要將對象加載到操作數(shù)棧畅涂。例如:

1、源碼(實例構(gòu)造器):
String str = new String();

字節(jié)碼:
0: new           #2 // class java/lang/String
3: dup
4: invokespecial #3 // Method java/lang/String."<init>":()V
7: astore_1
--------------------------------------------------------------------
2立宜、源碼(父類方法):
super.func();

字節(jié)碼:
0: aload_0
1: invokespecial #2 // Method com/Base.func:()V
--------------------------------------------------------------------
3橙数、源碼(私有方法):
funcPrivate();

字節(jié)碼:
0: aload_0
1: invokespecial #2 // Method funPrivate:()V

3.3 步驟三:動態(tài)分派(類使用時)

動態(tài)分派分為invokevitrual灯帮、invokeinterfaceinvokedynamic施流,其中動態(tài)調(diào)用invokedynamic是 JDK 1.7 新增的指令鄙信,我們單獨在另一篇中解析装诡。有些同學(xué)可能會覺得方法不重寫不就只有一個版本了嗎?這個想法忽略了Java動態(tài)鏈接的特性宾巍,Java可以從任何途徑加載一個class顶霞,除非解析的 5 種的情況外选浑,無法保證方法不被重寫。

invokevirtual指令

虛擬機為每個類生成虛方法表vtable(virtual method table)的結(jié)構(gòu)拓提,類中聲明的方法的入口地址會按固定順序存放在虛方法表中代态;虛方法表還會繼承父類的虛方法表,順序與父類保持一致疹吃,子類新增的方法按順序添加到虛方法末尾(這以Java單繼承為前提)蹦疑;若子類重寫父類方法,則重寫方法位置的入口地址修改為子類實現(xiàn)萨驶;

  • 1)類加載解析階段:解析類的繼承關(guān)系必尼,生成類的虛方法表 (包含了這個類型所有方法的入口地址)。舉個例子篡撵,有Class B繼承與Class A,并重寫了A中的方法:

Object是所有類的父類豆挽,所有每個類的虛方法表頭部都會包含Object的虛方法表。另外帮哈,B重寫了A#printMe()膛檀,所以對應(yīng)位置的入口地址方法被修改為B重寫方法的入口地址。

需要注意的是娘侍,被final咖刃、staticprivate修飾的方法不會出現(xiàn)在虛方法表中,因為這些方法無法被繼承重寫憾筏。

  • 2)調(diào)用階段(動態(tài)分派):解析階段生成虛方法表后嚎杨,每個方法在虛方法表中的索引是固定的,這是不會隨著實際類型變化影響的氧腰。調(diào)用方法時枫浙,首先根據(jù)變量的實際類型獲得對應(yīng)的虛方法表(包含了這個類型所有方法的入口地址),然后根據(jù)索引找到方法的入口地址古拴。
invokeinterface指令

接口方法的選擇行為與類方法的選擇行為略有區(qū)別箩帚,主要原因是Java接口是支持多繼承的,就沒辦法像虛方法表那樣直接繼承父類的虛方法表黄痪。虛擬機提供了itable(interface method table)來支持多接口紧帕,itable由偏移量表offset table與方法表method table兩部分組成。

當(dāng)需要調(diào)用某個接口方法時桅打,虛擬機會在offset table查找對應(yīng)的method table是嗜,隨后在該method table上查找方法愈案。

3.4 性能對比

  • invokestatic & invokespecial可以直接調(diào)用方法入口地址,最快
  • invokevirtual通過編號在vtable中查找方法叠纷,次之
  • invokeinterface現(xiàn)在offset table中查找method table的偏移位置刻帚,隨后在method table中查找接口方法的實現(xiàn)

4. 總結(jié)

  • 方法調(diào)用的本質(zhì)是從符號引用轉(zhuǎn)換到直接引用(方法入口地址)的過程,一共需要經(jīng)過(編譯時)生成符號引用涩嚣、(類加載時)解析崇众、(調(diào)用時)動態(tài)分派三個步驟
  • invokestatic & invokespecial指令在(類加載時)解析時根據(jù)靜態(tài)類型完成轉(zhuǎn)換
  • invokevirtual & invokeinterface在(調(diào)用時)根據(jù)實際類型,查找vtable & itable完成轉(zhuǎn)換
  • 重載其實是編譯器的語法特性與多態(tài)無關(guān)航厚,對編譯時符號引用生成有影響顷歌,在運行時已經(jīng)沒有影響了;重寫是多態(tài)的基礎(chǔ)幔睬,虛擬機通過vtable & itable來支持虛方法的方法選擇眯漩。

參考資料

  • 《深入理解Java虛擬機(第3版本)》(第8章)—— 周志明 著
  • 《深入理解Android:Java虛擬機 ART》(第2章) —— 鄧凡平 著
  • 《深入理解 JVM 字節(jié)碼》(第2、3章)—— 張亞 著

創(chuàng)作不易麻顶,你的「三連」是丑丑最大的動力赦抖,我們下次見!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末辅肾,一起剝皮案震驚了整個濱河市队萤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌矫钓,老刑警劉巖要尔,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異新娜,居然都是意外死亡赵辕,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門概龄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來还惠,“玉大人,你說我怎么就攤上這事旁钧∥兀” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵歪今,是天一觀的道長嚎幸。 經(jīng)常有香客問我,道長寄猩,這世上最難降的妖魔是什么嫉晶? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上替废,老公的妹妹穿的比我還像新娘箍铭。我一直安慰自己,他們只是感情好椎镣,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布诈火。 她就那樣靜靜地躺著,像睡著了一般状答。 火紅的嫁衣襯著肌膚如雪冷守。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天惊科,我揣著相機與錄音拍摇,去河邊找鬼。 笑死馆截,一個胖子當(dāng)著我的面吹牛充活,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蜡娶,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼混卵,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了窖张?” 一聲冷哼從身側(cè)響起淮菠,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎荤堪,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體枢赔,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡澄阳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了踏拜。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碎赢。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖速梗,靈堂內(nèi)的尸體忽然破棺而出肮塞,到底是詐尸還是另有隱情,我是刑警寧澤姻锁,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布枕赵,位于F島的核電站,受9級特大地震影響位隶,放射性物質(zhì)發(fā)生泄漏拷窜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望篮昧。 院中可真熱鬧赋荆,春花似錦、人聲如沸懊昨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽酵颁。三九已至嫉你,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間材义,已是汗流浹背均抽。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留其掂,地道東北人油挥。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像款熬,于是被迫代替她去往敵國和親深寥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359