一道題引起的慘案(加深理解)

題目:代碼的輸出?java

public class Base
{
    private String baseName = "base";
    public Base()
    {
        callName();
    }
 
    public void callName()
    {
        System. out. println(baseName);
    }
 
    static class Sub extends Base
    {
        private String baseName = "sub";
        public void callName()
        {
            System. out. println (baseName) ;
        }
    }
    public static void main(String[] args)
    {
        Base b = new Sub();
    }
}

答案:null函數

解答:this


咱們在仔細的觀察一下題目,能夠知道,這道題無非就是考察咱們三個知識點,第一,類的加載機制以及類的初始化過程;第二,繼承的相關知識,其中這裏涉及到子類繼承父類的時候,同名的屬性不會覆蓋父類,只是會將父類的同名屬性隱藏;第三,多態性,多態性就是讓實現與接口進行分離,在這道題目中,在父類的構造方法中調用了虛函數形成多態debug

居然咱們上面就提到這個題目就是考察咱們三個知識點,那麼咱們就根據題目對這三個知識點進行逐一擊破code

1.類加載的機制和程序運行的順序對象

咱們經過 Debug 能很好的瞭解程序的運行順序,由於 new 了一個 Sub 對象,且 Sub 類中沒有重寫構造函數,所以會調用父類的構造函數,父類 Base 的構造函數中調用了 callName 方法,所以就在父類的 callName 方法中的輸出語句打一個斷點,最後由於子類的 Sub 重寫了 callName 方法, 所以也在子類中重寫的 callName 方法中打一個斷點。最後經過 debug 咱們能夠看出程序的運行順序blog


 

知道了程序的運行順序以後,咱們還需知道一個知識點,那就是類的實例變量的初始化過程,也就是題目中成員變量 baseName 的初始化過程。繼承

咱們都知道,一個類一旦被加載鏈接初始化,它就能夠隨時被使用了,程序能夠訪問它的靜態字段,調用靜態方法,或者建立它的實例。在 JAVA 程序中類能夠被明確或者隱含地實例化有四種途徑:(1)明確使用 new 操做符;(2)調用 Class 或者 Constructor 對象的 newInstance() 方法;(3)調用任何現有對象的 clone() 方法;(4)或者經過 objectInputStream 類的 getObject() 方法反序列化。虛擬機建立一個新的實例時,都須要在堆中爲保存對象的實例分配內存。全部在對象的類中和它的父類中聲明的變量(包括隱藏的實例變量)都要分配內存。一旦虛擬機爲新的對象準備好堆內存,它當即把實例變量初始化爲默認的初始值。接口

2.繼承內存

題目中 Sub 類繼承了 Base 類,關於繼承,一個基本全部人都知道的知識點,不過這裏仍是貼出來

Java保證了一個對象被初始化前其父類也必須被初始化。有下面機制來保證:Java強制要求任何類的構造函數中的第一句必須是調用父類構造函數或者是類中定義的其餘構造函數。若是沒有構造函數,系統添加默認的無參構造函數,若是咱們的構造函數中沒有顯示的調用父類的構造函數,那麼編譯器自動生成一個父類的無參構造函數

3.多態

父類中的構造函數調用了 callName 方法,在題目中是經過 new Sub() 對象,所以調用的是子類 Sub 類中的 callName 方法,所以當前的 this 是指 Sub 類中的。

好了,最後咱們根據運行順序分析整個過程
1.Base b = new Sub();

在 main 方法中聲明父類變量b對子類的引用,JAVA類加載器將Base,Sub類加載到JVM;也就是完成了 Base 類和 Sub 類的初始化

2.JVM 爲 Base,Sub 的的成員開闢內存空間且值均爲 null

在初始化 Sub 對象前,首先 JAVA 虛擬機就在堆區開闢內存並將子類 Sub 中的 baseName 和父類 Base 中的 baseName(已被隱藏)均賦爲 null ,至於爲何 Base 類中的 baseName 爲何會被隱藏,上面的知識點也已經說明,就是子類繼承父類的時候,同名的屬性不會覆蓋父類,只是會將父類的同名屬性隱藏

3.調用父類的無參構造

調用 Sub 的構造函數,由於子類沒有重寫構造函數,默認調用無參的構造函數,調用了 super() 。

4.callName 在子類中被重寫,所以調用子類的 callName();

調用了父類的構造函數,父類的構造函數中調用了 callName 方法,此時父類中的 baseName 的值爲 base,但是子類重寫了 callName 方法,且 調用父類 Base 中的 callName 是在子類 Sub 中調用的,所以當前的 this 指向的是子類,也就是說是實現子類的 callName 方法

5.調用子類的callName,打印baseName

實際上在new Sub()時,實際執行過程爲:

public Sub(){
    super();
    baseName = "sub"; 
}

可見,在 baseName = "sub" 執行前,子類的 callName() 已經執行,因此子類的 baseName 爲默認值狀態 null 。

main 方法調用b.callName(),輸出的是base  由於java中,向上造型呈現的多態性僅僅針對成員函數,成員屬性不具備多態性.
b.callName(); //調用的是子類的函數(動態綁定)
b.baseName; // 調用的是父類的屬性

構造器的初始化順序大概是:父類靜態塊 ->子類靜態塊 ->父類初始化語句 ->父類構造函器 ->子類初始化語句 ->子類構造器。

做者:fuck兩點水 連接:https://www.jianshu.com/p/39f91f3fba32

相關文章
相關標籤/搜索