[短文速讀] 重載有暗坑,JVM是如何執行方法的
這將是一個系列文章。原因是自己寫了很多文章,也看了很多文章。從最開始的僅僅充當學習筆記,到現在認認真真去寫文章去分享。中間發現了很多事情,其中最大發現是:收藏不看!總是想著先收藏以後有時間再看,到後來…大家都懂得。大多數文章仿佛石沈大海,失去了應有的價值。
因為技術文章大多需要比較重的思考,但是現如今時間碎片化很嚴重,因此收藏不看也實屬不得已。所以萌生了這個系列的想法,系列文章的特點:以一些日常開發中不起眼的基礎知識點為內核,圍繞此包裹通俗易懂的文字。盡量用少思考的模式去講述一個知識。讓我們能夠真正在碎片化的時間裏學到東西!
出場角色
小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是靜態類型
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是如何執行方法的