爲了不沒必要要的浪費時間,文章主要是圍繞倆點進行展開:java
一、重載爲何根據靜態類型,而非動態類型?面試
二、經過動/靜態分派決定調用對應方法的符號引用。編程
若是對這倆個問題理解的比較深入的話,這篇文章不看也罷,哈哈~安全
文章後半部分,會從字節碼層面,聊一聊符號引用。若是Class文件結構不是很瞭解的小夥伴,能夠選擇性觀看~或者看看這篇文章,[動態代理三部曲:中] - 從動態代理,看Class文件結構定義多線程
愛因斯坦:「若是你不能簡單地解釋同樣東西,說明你沒真正理解它。」ide
[短文速讀-2] 重載/重寫,動/靜態分派?(從新修訂)學習
[短文速讀-3] 內部匿名類使用外部變量爲何要加finalspa
小A:MDove,我最近遇到一個問題百思不得其解。
MDove:正常,畢竟你這智商1+1都不知道爲何等於2。
小A:那1+1爲啥等於2呢?
MDove:......說你遇到的問題。
小A:是這樣的,我在學習多態的時候,重載和重寫,有點蒙圈了。我本身寫了一個重載和重寫的demo...
public class MethodMain {
public static void main(String[] args) {
MethodMain main = new MethodMain();
Language language = new Language();
Language java = new Java();
//重載
main.sayHi(language);
main.sayHi(java);
//重寫
language.sayHi();
java.sayHi();
}
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 {
@Override
public void sayHi() {
System.out.println("Hi,Im Java");
}
}
public class Language {
public void sayHi() {
System.out.println("Hi,Im Language");
}
}
複製代碼
小A:重寫的結果這個毫無疑問。可是爲何重載的demo運行結果是這個呀?我以爲它應該一個是Im Language一個是Hi Java呀。畢竟我在調用方法時,參數一個傳的實例化的類型一個Java,一個是Languae,爲啥不一個匹配參數是Java類型,一個匹配參數Language類型啊?
MDove:原來是這個疑惑呀。其實我最初也有這個疑惑。這裏借用 R大的一個回答,看回答以前,咱們先明確一個概念:A a = new B()
。這個A a中的A被稱之爲靜態類型,B稱爲動態類型/實際類型。
MDove:明確這個概念以後,讓咱們看一下R大的回答。(這裏我抽取了和這個問題相關的內容,若是想了解更多內容,能夠去R大的回答裏膜拜。)
爲什麼斷定調用哪一個版本的重載只經過傳入參數的靜態類型來斷定,而不使用其動態類型(運行時傳入的實際參數的實際類型)來斷定?其實根源並不複雜:由於這是當時的常規作法,C++也是這麼設計的,因而Java就繼承了下來。這樣作的好處是設計與實現都簡單,並且方法重載在運行時沒有任何額外開銷...(省略部份內容)...而這麼作的缺點也很明顯:犧牲了靈活性。若是程序確實須要根據多個參數的實際類型來作動態分派,那就得讓碼農們本身擼實現了。
小A:沒想到是出於這麼一種考慮。那咱們又是重載,又是重寫。這麼多很相似的方法。JVM是怎麼選擇和調用具體的方法的呢?
MDove:這個過程算是方法的調用,提到這個過程,咱們不得不聊一聊分派這個概念。分派又分兩種方式:靜態分派、動態分派。這兩種方式決定了一個方法的最終執行。
MDove:小A,你覺不以爲這兩個demo在寫法上有明顯的不一樣麼?或者再上升一個高度。重載和重寫是否是在業務場景上是有不一樣之處的?
小A:你這麼一說好像真是!重載是在一個類裏邊折騰;而重寫是子類折騰父類。
MDove:沒錯,正是如此!咱們總結一下:
MDove:上述你寫的那個重載demo裏,對於Language language = new Java();
來講:Language是靜態類型,Java是實際類型。一樣MethodMain main = new MethodMain();
也是如此,MethodMain main
這個MethodMain是靜態類型,new MethodMain()
這是MethodMain是實際類型。
MDove:因此,對於JVM來講,在編譯期參數/及調用者的靜態類型是肯定的,所以這一步方法的符號引用是肯定的。(這個過程就是:靜態分派)
小A:哦,原來在編譯期就已經肯定了符號引用...不對,等等!!若是肯定符號引用也會用過調用者的靜態類型,那重寫不也是調用靜態類型裏邊的方法了?!!!
MDove:哎呦,你小子反應的還挺快!先回答你的問題,在這個過程當中,重寫的確和重載所肯定的符號引用是同樣的!咱們看一下你demo中字節碼的內容,這裏只截取一部分:
紅色是重載,紫色是重寫
MDove:看到了吧,你說的沒錯,紫色所標註的內容就是重寫的倆個方法在編譯期決定的符號引用。由於靜態分派的緣由,它們倆的確是相同的!而重載的方法選擇就是在這個過程肯定的,可是重寫比較特殊,它須要依賴運行時對象的實際類型,所以對於重寫來講它還須要動態分派。
MDove:對於靜態分派。說白了就是,在編譯期就決定好,調用哪一個版本的方法。所以對於在運行期間生成的實際類型JVM是不關心的。只要你的靜態類型是郭德綱,就算你new一個吳亦凡出來。這行代碼也不能又長又寬...
MDove:這裏總結一下,全部依賴靜態類型來定位方法執行版本的方式都稱之爲:靜態分派。
小A:靜態分派我明白了,快講講動態分派吧!我想知道爲何一樣的符號引用,重寫會執行不一樣的方法!
MDove:咱們都知道重寫涉及到你是調用子類的方法仍是調用父的。也就是說調用者的實際類型對於重寫是有影響的,所以這種狀況下僅靠靜態分派就行不通了,須要在運行期間的動態分派去進一步肯定。
MDove:此次咱們來看一下這個main方法裏邊的字節碼狀況:
MDove:這裏簡單的解釋一下這些字節碼指令:
第一個紫色的圈:執行常量池#5的符號應用,並將其壓倒操做數棧第2個位置。
第一個紫色的線:就是常量池#5對應的符號引用。咱們能夠看出它是一個Language的類型。
第一個紅色的圈:執行常量池#6的符號應用,並將其壓倒操做數棧第3個位置。
第一個紅色的線:就是常量池#6對應的符號引用。咱們能夠看出它是一個Java的類型
第二個紫色的圈:取出操做數棧2的變量,執行invokevirtual執行也就是執行#9。
第二個紫色的線:就是常量池#9對應的符號引用。咱們能夠看出它Language.sayHi()方法。
第二個紅色的圈:取出操做數棧3的變量,執行invokevirtual執行也就是執行#9。
第二個紅色的線:就是常量池#9對應的符號引用。咱們能夠看出它Language.sayHi()方法。
MDove:經過字節碼指令咱們能夠看出,除了操做數棧中變量的類型不一樣,其餘的都是相同的。所以動態分派的特性,就在invokevirtual這個執行中。
MDove:簡單來講,虛擬機在執行invokevirtual時,會先找到操做數棧頂第一個元素,去找它的實際類型,而後找到它對應的符號引用,若是沒有就一步步往上找,直到找到。緊接着動態連接到它的真正內存地址,完成方法調用。
MDove:對應到咱們的demo,上就是:執行java.sayHi()時,先是aload_3出棧,肯定它的實際類型,一看是Java類型,因此就會在Java.class中找對應的符號引用,有的話,返回對應的符號引用,執行。也就完成了咱們的重寫調用。
小A:Java真好玩,我想回家送外賣...
對於重載來講,在編譯期,就已經經過靜態類型決定要選擇那個版本的方法了(決定調用哪一個符號引用)。而這種經過靜態類型來定位方法執行版本的過程叫作:靜態分派。
對於重寫來講,經過靜態類型顯然是行不通的,所以須要動態類型來判斷。那麼這種經過動態類型來定位方法執行版本的過程叫作:動態分派。