從入門到放棄之JVM角度看Java多態

首先,明確一下,Java多態的三個必要條件:

1、 繼承

2、 子類重寫父類方法

3、 父類引用指向子類對象

然後看一個例子

package test.xing;

class Father{
    protected int age;
    public Father(){
        age = 40;
    }
    //加入Java開發交流君樣:756584822一起吹水聊天
    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 );
        //加入Java開發交流君樣:756584822一起吹水聊天
    }

}

輸出結果爲:

給出結論:當滿Java多態的三個條件時,可以發現c.eat()調用的實際上是子類的eat,但c.age調用的還是父類的age,而c.play()則不會通過編譯。

下面從JVM的角度解釋上面這種現象

  • 我們就從Father c = new Child()這句話切入

這句話首先會執行new Child(),在堆中分配一個對象。

當然在分配Child類的實例時,先要通過JVM的類加載器將Child類對應的class文件加載到JVM中,然後JVM根據class文件中的字節流產生一個表示class文件中類型信息的結構體

這個結構體的具體實現,不同的JVM會有不同的實現方式,但大致上都差不多,都要遵守JVM的規範。

這個表示class文件中類型信息的結構體大概由以下幾部分構成:

  • 1、 常量池

  • 2、 類變量(靜態變量)

  • 3、 字段信息

  • 4、 方法信息

  • 5、 類的父類信息

  • 6、 類的訪問權限信息等

這些我說的也不夠準確,具體的大家可以看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就必須四個字節四個字節地訪問內存。
//加入Java開發交流君樣:756584822一起吹水聊天
但是我們都知道指針是不安全的,其中一個不安全因素就是指針可能訪問到沒有分配的內存空間,也就是說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行的內容!!!!
//加入Java開發交流君樣:756584822一起吹水聊天
然而前面說過,在方法表的形成過程中,子類重寫的方法會覆蓋掉表中原來的數據,也就是Child類的方法表的第三行是指向Child.eat的引用,而不是指向Parent.eat(因爲方法表產生了覆蓋),所以c訪問到的是Child.eat。也就是子類的方法!!!這種情況下,c是沒有辦法直接訪問到父類的eat方法的。


以上就是對輸出結果的解釋。

image

最新2020整理收集的一些高頻面試題(都整理成文檔),有很多幹貨,包含mysql,netty,spring,線程,spring cloud、jvm、源碼、算法等詳細講解,也有詳細的學習規劃圖,面試題整理等,需要獲取這些內容的朋友請加Q君樣:756584822

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章