Java的多態為何可以由子類實例化父類?


Java的多態為何可以由子類實例化父類?

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

1、 繼承

2、 子類重寫父類方法

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

然后看一個例子

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 );

 

}

 

}

輸出結果為:

 

 

給出結論:當滿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就必須四個字節四個字節地訪問內存。

但是我們都知道指針是不安全的,其中一個不安全因素就是指針可能訪問到沒有分配的內存空間,也就是說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方法的。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM