2015攜程JAVA工程師筆試題(基礎卻又沒多少人做對的面向對象面試題)
最近真的發現自己越來越懶了,雖然現在有點晚了,可是內疚完之后,還是得更新每個星期一篇的面試題,找了很多面試題,發現有些很基礎的就沒必要分享出來,最后找了這么一篇經典的面試題,一開始我也還真做錯了,話不多說。來看下 2015 攜程的 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.png
可以看到,結果為 null 。為什么呢?
我們在仔細的觀察一下題目,可以知道,這道題無非就是考察我們三個知識點,第一,類的加載機制以及類的初始化過程;第二,繼承的相關知識,其中這里涉及到子類繼承父類的時候,同名的屬性不會覆蓋父類,只是會將父類的同名屬性隱藏;第三,多態性,多態性就是讓實現與接口進行分離,在這道題目中,在父類的構造方法中調用了虛函數造成多態
竟然我們上面就提到這個題目就是考察我們三個知識點,那么我們就根據題目對這三個知識點進行逐一擊破
1.類加載的機制和程序運行的順序
我們通過 Debug 能很好的了解程序的運行順序,因為 new 了一個 Sub 對象,且 Sub 類中沒有重寫構造函數,因此會調用父類的構造函數,父類 Base 的構造函數中調用了 callName 方法,因此就在父類的 callName 方法中的輸出語句打一個斷點,最后因為子類的 Sub 重寫了 callName 方法, 因此也在子類中重寫的 callName 方法中打一個斷點。最后通過 debug 我們可以看出程序的運行順序
Debug查看運行的順序.png
知道了程序的運行順序之后,我們還需知道一個知識點,那就是類的實例變量的初始化過程,也就是題目中成員變量 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 。
時間也很晚了(2016年12月27日01:35:54),最后給出一個知識點就睡了
構造器的初始化順序大概是:父類靜態塊 ->子類靜態塊 ->父類初始化語句 ->父類構造函器 ->子類初始化語句 子類構造器。
來自:http://www.jianshu.com/p/39f91f3fba32