1. 程式人生 > >[短文速讀] 重載有暗坑,JVM是如何執行方法的

[短文速讀] 重載有暗坑,JVM是如何執行方法的

amp 抽象 開發 java虛擬機規範 很好 extends -c nds 不同的

前言

這將是一個系列文章。原因是自己寫了很多文章,也看了很多文章。從最開始的僅僅充當學習筆記,到現在認認真真去寫文章去分享。中間發現了很多事情,其中最大發現是:收藏不看!總是想著先收藏以後有時間再看,到後來…大家都懂得。大多數文章仿佛石沈大海,失去了應有的價值。

因為技術文章大多需要比較重的思考,但是現如今時間碎片化很嚴重,因此收藏不看也實屬不得已。所以萌生了這個系列的想法,系列文章的特點:以一些日常開發中不起眼的基礎知識點為內核,圍繞此包裹通俗易懂的文字。盡量用少思考的模式去講述一個知識。讓我們能夠真正在碎片化的時間裏學到東西!

出場角色

小A:剛踏入Java編程之路…

MDove:一個快吃不上飯的Android開發…

正題

引子

小A:MDove,我最近遇到一個問題百思不得其解。

MDove:正常,畢竟你這智商1+1都不知道為什麽等於2。

小A:那1+1為啥等於2呢?

MDove:......說你遇到的問題。


重載不理解

小A:是這樣的,我在學習多態的時候,重載和重寫,有點蒙圈了...

public class MethodMain {
    public static void main(String[] args) {
        MethodMain main = new MethodMain();
        Language language = new MethodMain().new Java();
        Language java = new MethodMain().new Java();

        main.sayHi(language);
        main.sayHi(java);
    }

    private void sayHi(Java java) {
        System.out.println("Hi Java");
    }

    private void sayHi(Language language) {
        System.out.println("Im Language");
    }

    public class Java extends Language {}
    public abstract class Language {}
}

技術分享圖片

小A:程序運行結果為什麽是這個呀?我覺得它應該一個是Im Language一個是Hi Java呀。

MDove:原來是這個疑惑呀。好,那今天就好好聊一聊重載/重寫背後:方法調用的原理。為了更好理解,我盡量不用學術性強的語言來解釋。開始之前讓我們先看一行代碼:

如果想了解更專業的內容,可以參考《Java虛擬機規範》或者《深入理解Java虛擬機》。

A a = new B();

MDove:對於A和B來說,他們有不同的學術名詞。A稱之為靜態類型,B稱之為實際類型。對於Language language = new MethodMain().new Java();也是如此:Language是靜態類型

,Java是實際類型

MDove:從你寫的demo裏,我們可以看出來:main.sayHi(language); main.sayHi(java);最終都是調用了private void sayHi(Language language)。我們是不是可以得出一個結論:方法的調用是根據靜態類型去匹配的?
就像你的那個demo一樣,language和java的靜態類型都是Language所以就匹配了private void sayHi(Language language)這個方法。


重寫不明白

小A:不對啊!!!如果用Override重寫的話,這個結論是不成立的!按照靜態類型的說法,那下面那個demo,的調用者的靜態類型是Language應該打印Hi,Im Language。

public class MethodMain {
    public static void main(String[] args) {
        Language language = new MethodMain().new Java();

        language.sayHi();
    }

    public class Java extends Language {
        @Override
        public void sayHi() {
            System.out.println("Hi,Im Java");
        }
    }

    public class Language {
        public void sayHi() {
            System.out.println("Hi,Im Language");
        }
    }
}

技術分享圖片

MDove:別急,你這是面向對象多態神經紊亂綜合征。說白了就是看串了。你難道不覺得,這倆個demo寫法上有不同麽?或者再上升一下重載和重寫是不是有不同之處?

小A:你這麽一說好像真是!重載是在一個類裏邊折騰;而重寫子類折騰父類

MDove:沒錯,正式如此。導致了JVM在加載方法的時候采用了不同的方式。因此也就有了你所感到疑惑的,為什麽重載會是這種結果,而重寫會是那種結果。

小A:那可不可以最多講一講加載方法的不同之處的?

JVM如何調用方法

MDove:在調用之前,我們再回到上文提到的靜態類型上。對於JVM來說,在編譯期變量的靜態類型是確定的,同樣重載的方法也就能夠確定。很好理解,因為二者都是確定無誤的。所以對於這種方法,JVM采用靜態分派的方式去調用。(因為這類不涉及任何需要動態決定類型的地方)

MDove:說白了就是,在編譯期就決定好該怎麽調用這個方法。因此對於在運行期間生成的實際類型JVM是不關心的。只要你的靜態類型是郭德綱,就算你new一個吳亦凡出來。這行代碼也不能又長又寬...

小A:照這個邏輯來說,重寫就是動態分派,需要JVM在運行期間確定對象的實際類型,然後再決定調用哪個方法。

MDove:沒錯,畢竟重寫涉及到你是調用子類的方法還是調用父類。也就是說調用者的類型對於重寫是有影響的,因此這種情況下靜態分派就行不通了,需要在運行期間去決定。

MDove:當然我們用嘴說是很輕巧的,實際JVM去執行時是很復雜的過程。如果你感興趣可以去了解這方面的知識。簡單來說,虛擬機在執行對應的字節碼時,變量壓棧之後,會去找它的實際類型,然後在將它的符號引用映射到真正的方法地址上,也就是我們子類重寫的方法上。

重載的暗坑

MDove:因為重載的性質,重載在可變參數上是有坑的。我寫的demo,你瞅瞅能不能感覺出奇怪的地方:

public class MethodMain {
    public static void main(String[] args) {
        MethodMain main = new MethodMain();
        main.fun(null, 666);
        main.fun(null, 666, 666);
    }

    private void fun(Object obj, Object... args) {
        System.out.println("fun(Object obj, Object... args)");
    }

    private void fun(String string, Object obj, Object... args) {
        System.out.println("fun(String string, Object obj, Object... args)");
    }
}

小A:我覺得應該是打印:fun(Object obj, Object... args)和fun(String string, Object obj, Object... args)吧?

MDove:最開始我也是這麽認為的。我們從我們的角度出發,很自然的認為main.fun(null, 666);應該調用private void fun(Object obj, Object... args),而main.fun(null, 666, 666);去調用private void fun(String string, Object obj, Object... args)

MDove:可以如果我們站在程序的角度呢?因為我們寫的是可變參數,程序怎麽可能知道666和666,666應該去對應哪個方法。所以這個demo的結果是:

技術分享圖片

小A:那我有一個疑問,既然程序很難洞察應該調用哪個可變參數的方法,那它又是為什麽調用了下邊的而不是上邊的呢?

MDove:那是因為編譯期在匹配方法時,如果有多個可能性,它會使用更向下的類型,結合上述的demo。因為我們傳入null時,雖然即能滿足Object又能滿足String。但由於String是 Object的子類(也就是更向下),因此編譯器會認為第二個方法更為貼切。

小A:Skr,Skr...

static重寫

MDove:我們繼續聊一聊重寫,咱們說了普通的重寫。靜態的重寫豈能不提。首先來說static不能稱之為重寫,只能叫做隱藏父類實現。文字很抽象,直接看代碼:

public class MethodMain {
    public static void main(String[] args) {
        Language.sayHi();
        Java.sayHi();
    }
}

public class Java extends Language {
    public static void sayHi() {
        System.out.println("Hi,Im Java");
    }
}

public class Language {
    public static void sayHi() {
        System.out.println("Hi,Im Language");
    }
}

技術分享圖片

MDove:說白了就是:老子是老子的,兒子是兒子的。其實這個也比較好理解。static的變量、方法都是伴隨類存在的,類加載完畢就生成了。它和對象new不new是沒有關系的。因此也不存在什麽實際變量一說。因此也就有了上邊的這種情況,也就是常說的:隱藏父類。

小A:這些內容,學習的時候還真沒有好好的去思考...以後要加油了!

劇終

[短文速讀] 重載有暗坑,JVM是如何執行方法的