首先,明確一下,Java多態的三個必要條件:java
一、 繼承程序員
二、 子類重寫父類方法安全
三、 父類引用指向子類對象spa
而後看一個例子指針
package test.xing; class Father{ protected int age; public Father(){ age = 40; } void eat(){ System.out.println("父親在吃飯"); } } class Child extends Father{ protected int age; public Child(){ age = 18; } void eat(){ System.out.println("孩子在吃飯"); } void play(){ System.out.println("孩子在打CS"); } } public class TestPolymorphic { public static void main(String[] args) { Father c = new Child(); c.eat(); //c.play(); System.out.println("年齡:"+c.age ); } }
輸出結果爲:code
給出結論:當滿Java多態的三個條件時,能夠發現c.eat()調用的其實是子類的eat,但c.age調用的仍是父類的age,而c.play()則不會經過編譯。對象
下面從JVM的角度解釋上面這種現象blog
咱們就從Father c = new Child()這句話切入繼承
這句話首先會執行new Child(),在堆中分配一個對象。內存
固然在分配Child類的實例時,先要經過JVM的類加載器將Child類對應的class文件加載到JVM中,而後JVM根據class文件中的字節流產生一個表示class文件中類型信息的結構體
這個結構體的具體實現,不一樣的JVM會有不一樣的實現方式,但大體上都差很少,都要遵照JVM的規範。
這個表示class文件中類型信息的結構體大概由如下幾部分構成:
一、 常量池
二、 類變量(靜態變量)
三、 字段信息
四、 方法信息
五、 類的父類信息
六、 類的訪問權限信息等
這些我說的也不夠準確,具體的你們能夠看JVM相關的書籍如《深刻理解Java虛擬機》,在這裏就有個大概的概念就好。
以後,JVM會根據上面這個結構體生成一個叫作方法表的東西。這個方法表是實現java多態的一個關鍵。
方法表中包含的是實例方法(就是相對於靜態方法而言的,用對象訪問的那些方法)的直接引用,也就是說經過這個方法表就可以訪問到該類的實例方法,
並且,這些實例方法不只包括本類的方法,還包括其父類的實例方法,以及父類的父類的實例方法(就是一直到Object)。
並且,這些方法中不包含私有方法(由於私有方法不能繼承)
方法表中的這些直接應用會指向到JVM中表示類型信息的那個結構體(就是上面那個結構體)的相應的方法信息(就是上面結構體中4的某個位置),固然這只是本類的方法,表中還有父類的方法,相應地指向父類類型信息結構體的具體位置。
可能表達的不夠清晰,下面畫個圖表示。
上面提到過,方法表中不只包括本類的方法,還包括父類的方法,方法表值這樣產生的,以Child類的方法表爲例:
首先方法表中,會產生指向繼承自Object類的方法的引用,這些包括指向toString的和指向equals的,固然Object中還包括不少方法,這裏就不寫了
而後方法表中產生指向繼承自Parent類的方法的引用,這包括eat,
最後產生指向本類的方法的引用。
這裏須要注意的一點是,當Child類的方法表產生指向Parent類中的方法的引用時,會有一個指向eat方法的引用,最後產生指向本類的方法的引用時,也有一個指向eat的引用,這時候,新的數據會覆蓋原有的數據,也就是說原來指向Parent.eat的那個引用會被替換成指向Child.eat的引用(佔據原來表中的位置)。因此咱們看到在Child類的方法表中指向的是Child.eat而Parent類的方法表中指向的是Parent.eat。子類的方法表中就沒有指向Parent.eat的引用了。
並且還要注意一個特色就是,Parent和Child的方法表中,指向eat的引用在表中的偏移量是同樣的,都是第三個位置。(這是由於子類eat方法覆蓋掉了父類eat方法,佔據了原來父類eat方法的引用在表中的位置)
這裏再多說一句,表示類型信息的結構體中,的方法信息,只包含本類特有的,或者是重寫的方法信息,沒有父類的方法信息。
瞭解了方法區的結構後,咱們來看堆中對象的結構
接下來是棧區,產生Father類型的引用,這個引用指向堆區中的Child類的實例。
這裏須要解釋一下Father c的含義,咱們知道c表示一個引用,這個引用指向堆中的Child類的實例,說白了就是一個地址,這個地址指向堆中的Child的類的實例,可是咱們不要忘記前面還有一個Father修飾這個c
咱們都知道在c中有void類型的指針,而給指針前面限定一個類型就限制了指針訪問內存的方式,好比char * p就表示p只能一個字節一個字節地訪問內存,可是int *p中p就必須四個字節四個字節地訪問內存。
可是咱們都知道指針是不安全的,其中一個不安全因素就是指針可能訪問到沒有分配的內存空間,也就是說char *雖然限制了p指針訪問內存的方式,可是沒有限制能訪問內存的大小,這一點要徹底靠程序員本身掌握。
可是在java的引用中Father不但指定了c以何種方式訪問內存,也規定了可以訪問內存空間的大小。
咱們看Father實例對象的大小是佔兩行,但Child實例對象佔三行(這裏就是簡單量化一下)。
因此雖然c指向的是Child實例對象,可是前面有Father修飾它,它也只能訪問兩行的數據,也就是說c根本訪問不到Child類中的age!!!只能訪問到Father類的age,因此輸出40
並且咱們注意兩個類的方法表:
咱們看到Parent的方法表佔三行,Child的方法表佔4行,c雖然指向了Child類的實例對象,而對象中也有指針指向Child類的方法表,可是因爲c受到了Father的修飾,經過c也只能訪問到Child方法表中前3行的內容!!!!
然而前面說過,在方法表的造成過程當中,子類重寫的方法會覆蓋掉表中原來的數據,也就是Child類的方法表的第三行是指向Child.eat的引用,而不是指向Parent.eat(由於方法表產生了覆蓋),因此c訪問到的是Child.eat。也就是子類的方法!!!這種狀況下,c是沒有辦法直接訪問到父類的eat方法的。
以上就是對輸出結果的解釋。
花了大概兩天的時間看JVM虛擬機,看得不夠仔細,紕漏之處還請之處。謝謝。