從JVM角度看Java多態

首先,明確一下,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虛擬機,看得不夠仔細,紕漏之處還請之處。謝謝。

相關文章
相關標籤/搜索