我們都知道Java有三大寶,分別是:多態、封裝、繼承。其中多態主要體現就是重寫與重載(有些人認為重載根本不屬於多態)兩種方式,那么今天就結合研讀過JVM之后自己的理解來聊聊重載與重寫的VM內部是怎么實現的,是怎么找到最后執行的方法的。
在分析重載與重寫這兩個之前,我們必須要知道一些概念:分派、靜態分派、動態分派、實際類型、靜態類型....(之后涉及到的會邊介紹別舉例才能更好地理解)
一、相關的概念
1、靜態類型與實際類型
先看以下一個重載例子的輸出,再進一步介紹:
1 public class StaticDispatch { 2 3 static abstract class Human{ } 4 static class Man extends Human{ } 5 static class Women extends Human{ } 6 //三個方法重載 7 public void sayHello(Human guy) { 8 System.out.println("hello guy!"); 9 } 10 public void sayHello(Man guy) { 11 System.out.println("hello gentleman!"); 12 } 13 public void sayHello(Women guy) { 14 System.out.println("hello laday!"); 15 } 16 public static void main(String[] args) { 17 Human man = new Man();//upcast 18 Human woman = new Women();//upcast 19 StaticDispatch sd = new StaticDispatch(); 20 //輸出結果為:hello guy! 21 sd.sayHello(man); 22 sd.sayHello(woman); 23 } 24 }
大多數都應該能知道最后的輸出結果為:
hello, guy
hello, guy
這是為什么呢?根據輸出來看man與woman就是Human類,這肯定是成立的,通過向上轉型成基類(父類)可以使用getClass()或者instanceof確定,我們先不研究為什么吧。再通過重寫的一個例子:
1 public class DynamicDispatch { 2 static abstract class Human{ 3 protected void sayHello("hello, guy"); 4 } 5 static class Man extends Human{ 6 7 @Override 8 protected void sayHello() { 9 System.out.println("man say hello"); 10 } 11 } 12 static class Women extends Human{ 13 14 @Override 15 protected void sayHello() { 16 System.out.println("women say hello"); 17 } 18 } 19 public static void main(String[] args) { 20 Human man = new Man(); 21 Human women = new Women(); 22 man.sayHello(); 23 women.sayHello(); 24 man = new Women(); 25 man.sayHello(); 26 }
輸出結果:
man say hello
women say hello
women say hello
Man與Women重寫了基類Human的sayHello()方法,輸出的就是:man say hello women say hello而不是hello, guy,之后得特別注意man = new Women()並不是將man對象轉為Women對象,其實就是將再復制了一個指向Woment的對象引用賦值給man,或者說man的指向由之前的Man變為Women(可能描述不太准准確),所以其實man = new Women()的man就是實際類型Women的一個對象實例,本質就是為實際類型。輸出women say hello便能理解!
那么如果不重寫父類的方法呢?
1 public class DynamicDispatch { 2 static abstract class Human{ 3 protected void sayHello() { 4 System.out.println("hello, guy"); 5 }; 6 } 7 static class Man extends Human{ 8 9 /*@Override 10 protected void sayHello() { 11 //System.out.println("man say hello"); 12 } */ 13 } 14 static class Women extends Human{ 15 16 /*@Override 17 protected void sayHello() { 18 //System.out.println("women say hello"); 19 }*/ 20 } 21 public static void main(String[] args) { 22 Human man = new Man(); 23 Human women = new Women(); 24 man.sayHello(); 25 women.sayHello(); 26 man = new Women(); 27 man.sayHello(); 28 } 29 }
結果為:
hello, guy
hello, guy
hello, guy
這什么我們又會問這是為什么呢?先不急,我們先了解靜態類型、實際類型等幾個概念之后再進一步介紹。
(1)靜態類型:Human稱為變量的靜態類型Static Type或者外觀類型Apparent Type;靜態類型的變化只會在使用時發生,變量本身的靜態類型不會被改變。
(2)實際類型:Man/Women稱為變量的實際類型Actual Type;實際類型變化結果在運行期才能確定,編譯器並不知道對象的實際類型(可以看《Thinking in Java》中相關的描述)
(3)有一種說法就是:重載是靜態的,重寫是動態的,所以重寫算是多態性的體現,而重載不算是。各人有各人的理解,但是我覺得是這句話是顯然不能夠成立的,為什么呢?雖然重載最后確定使用哪個方法是完全通過參數類型與個數就能被確定的,但是Human man = new Man()類似於這樣的cast轉型,最后參數的類型也是“動態”的,cast之后才知道的。具體的還是先看下面的解釋吧,之后相信大家都會有個大概的概念(可能個人理解也會有偏差,望指正!)。
(4) 事實上,就是定義了兩個靜態類型相同、實際類型不同的變量。而重載方法是通過參數的靜態類型Human而不是實際類型Women/Man作為判斷。靜態類型在編譯期可知的,所以會選擇sayHello(Human guy)方法輸出。
2、靜態分派與動態分派
(1)靜態分派(《Thinking In Java》中稱之為靜態綁定(前期綁定)):所有依賴靜態類型來定位方法執行版本(版本即哪一個方法)的分派動作,靜態分派的最典型的應用就是方法重載。
(2)動態分派(《Thinking In Java》中稱之為動態綁定(后期綁定)):在運行期根據實際類型確定執行版本的分派過程稱為動態分派,這是重寫的實際本質,在重寫過程中並不是唯一的版本,而是選擇更加合適的版本(如果有多個父類,那么接近上層的優先級越低)。
3、多分派類型與單分派類型
(1)多分派類型:根據一個以上的宗量(方法的接受者與方法的參數統稱為方法的宗量)進行方法的選擇方法的分派類型。其中靜態分派屬於多分派類型。即Father father = new Son(); father.overloadMethod(param),中overloadMethod()方法的選擇是要根據靜態類型Father與方法的參數param共同確定的
(2)單分派類型:動態分配屬於單分派類型,即只會根據實際類型Son選擇方法。
那么廢話不多說,其實可以總結起來就是:Java語言是一門靜態多分派,動態單分派的語言。
二、VM中動態分派的實現
靜態分派相對好理解,只要確定靜態類型、參數以及參數個數就能知道最后是哪個版本被執行,但是動態分派就相對難理解,但是結合VM的相關知識理解,其實也就那么回事。
(1)高能權威解釋:
動態分配並不會頻繁操作去搜索合適的目標方法,而是通過在類的方法區中建立一個虛方法表(Virtual Method Table=vtable)來代替元數據查找以提高性能。Father與Son的虛方法表如下圖(Vtable中存放各個方法的實際入口地址)
(2)個人粗俗解釋:
動態分派(我)懶得每次都要浪費時間去找要被執行的目標方法,VM大哥本來就已經提供給了我方法表這么一個好東西,只要我加以利用(虛表中存放方法的實際入口)就能實現了找到目標方法。
先看下圖,之后解釋便會一目了然:
解釋1:如果子類沒有重寫方法,執行的是父類的方法:這是因為子類中沒有被重寫,那么子類的虛方法表里面的地址入口與父類相同方法的地址入口是一致的,都指向父類的實現入口。即:Father與Son的choiceA與choiceB方法都指向了Father相同方法中的入口地址。
解釋2:如果子類重寫父類方法,那么執行的是子類的方法,這是因為子類方法表中地址將會被替換為指向子類實現版本的入口地址。即:Son的choiceA與choiceB方法被重寫了,則指向了Son實現版本的入口地址,並沒有指向Father的箭頭。
解釋3:Father與Son從Object繼承來的方法都沒有重寫,故都會指向Object的數據類型;