最近真的發(fā)現(xiàn)自己越來越懶了,雖然現(xiàn)在有點(diǎn)晚了,可是內(nèi)疚完之后,還是得更新每個(gè)星期一篇的面試題,找了很多面試題,發(fā)現(xiàn)有些很基礎(chǔ)的就沒必要分享出來,最后找了這么一篇經(jīng)典的面試題,一開始我也還真做錯(cuò)了,話不多說。來看下 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();
}
}
求這段程序的輸出值?
二、解題
因?yàn)楸旧硪婚_始我也做錯(cuò)了這道題,因此不好寫一開始的思考思路,我們就用最直接的方法來看下答案是什么?
直接把程序運(yùn)行,看輸出的結(jié)果:

可以看到,結(jié)果為 null 。為什么呢?
我們?cè)谧屑?xì)的觀察一下題目,可以知道,這道題無非就是考察我們?nèi)齻€(gè)知識(shí)點(diǎn),第一,類的加載機(jī)制以及類的初始化過程;第二,繼承的相關(guān)知識(shí),其中這里涉及到子類繼承父類的時(shí)候,同名的屬性不會(huì)覆蓋父類,只是會(huì)將父類的同名屬性隱藏;第三,多態(tài)性,多態(tài)性就是讓實(shí)現(xiàn)與接口進(jìn)行分離,在這道題目中,在父類的構(gòu)造方法中調(diào)用了虛函數(shù)造成多態(tài)
竟然我們上面就提到這個(gè)題目就是考察我們?nèi)齻€(gè)知識(shí)點(diǎn),那么我們就根據(jù)題目對(duì)這三個(gè)知識(shí)點(diǎn)進(jìn)行逐一擊破
1.類加載的機(jī)制和程序運(yùn)行的順序
我們通過 Debug 能很好的了解程序的運(yùn)行順序,因?yàn)?new 了一個(gè) Sub 對(duì)象,且 Sub 類中沒有重寫構(gòu)造函數(shù),因此會(huì)調(diào)用父類的構(gòu)造函數(shù),父類 Base 的構(gòu)造函數(shù)中調(diào)用了 callName 方法,因此就在父類的 callName 方法中的輸出語句打一個(gè)斷點(diǎn),最后因?yàn)樽宇惖?Sub 重寫了 callName 方法, 因此也在子類中重寫的 callName 方法中打一個(gè)斷點(diǎn)。最后通過 debug 我們可以看出程序的運(yùn)行順序

知道了程序的運(yùn)行順序之后,我們還需知道一個(gè)知識(shí)點(diǎn),那就是類的實(shí)例變量的初始化過程,也就是題目中成員變量 baseName 的初始化過程。
我們都知道,一個(gè)類一旦被加載連接初始化,它就可以隨時(shí)被使用了,程序可以訪問它的靜態(tài)字段,調(diào)用靜態(tài)方法,或者創(chuàng)建它的實(shí)例。在 JAVA 程序中類可以被明確或者隱含地實(shí)例化有四種途徑:(1)明確使用 new 操作符;(2)調(diào)用 Class 或者 Constructor 對(duì)象的 newInstance() 方法;(3)調(diào)用任何現(xiàn)有對(duì)象的 clone() 方法;(4)或者通過 objectInputStream 類的 getObject() 方法反序列化。虛擬機(jī)創(chuàng)建一個(gè)新的實(shí)例時(shí),都需要在堆中為保存對(duì)象的實(shí)例分配內(nèi)存。所有在對(duì)象的類中和它的父類中聲明的變量(包括隱藏的實(shí)例變量)都要分配內(nèi)存。一旦虛擬機(jī)為新的對(duì)象準(zhǔn)備好堆內(nèi)存,它立即把實(shí)例變量初始化為默認(rèn)的初始值。
2.繼承
題目中 Sub 類繼承了 Base 類,關(guān)于繼承,一個(gè)基本所有人都知道的知識(shí)點(diǎn),不過這里還是貼出來
Java保證了一個(gè)對(duì)象被初始化前其父類也必須被初始化。有下面機(jī)制來保證:Java強(qiáng)制要求任何類的構(gòu)造函數(shù)中的第一句必須是調(diào)用父類構(gòu)造函數(shù)或者是類中定義的其他構(gòu)造函數(shù)。如果沒有構(gòu)造函數(shù),系統(tǒng)添加默認(rèn)的無參構(gòu)造函數(shù),如果我們的構(gòu)造函數(shù)中沒有顯示的調(diào)用父類的構(gòu)造函數(shù),那么編譯器自動(dòng)生成一個(gè)父類的無參構(gòu)造函數(shù)
3.多態(tài)
父類中的構(gòu)造函數(shù)調(diào)用了 callName 方法,在題目中是通過 new Sub() 對(duì)象,因此調(diào)用的是子類 Sub 類中的 callName 方法,因此當(dāng)前的 this 是指 Sub 類中的。
好了,最后我們根據(jù)運(yùn)行順序分析整個(gè)過程
1.Base b = new Sub();
在 main 方法中聲明父類變量b對(duì)子類的引用,JAVA類加載器將Base,Sub類加載到JVM;也就是完成了 Base 類和 Sub 類的初始化
2.JVM 為 Base,Sub 的的成員開辟內(nèi)存空間且值均為 null
在初始化 Sub 對(duì)象前,首先 JAVA 虛擬機(jī)就在堆區(qū)開辟內(nèi)存并將子類 Sub 中的 baseName 和父類 Base 中的 baseName(已被隱藏)均賦為 null ,至于為什么 Base 類中的 baseName 為什么會(huì)被隱藏,上面的知識(shí)點(diǎn)也已經(jīng)說明,就是子類繼承父類的時(shí)候,同名的屬性不會(huì)覆蓋父類,只是會(huì)將父類的同名屬性隱藏
3.調(diào)用父類的無參構(gòu)造
調(diào)用 Sub 的構(gòu)造函數(shù),因?yàn)樽宇悰]有重寫構(gòu)造函數(shù),默認(rèn)調(diào)用無參的構(gòu)造函數(shù),調(diào)用了 super() 。
4.callName 在子類中被重寫,因此調(diào)用子類的 callName();
調(diào)用了父類的構(gòu)造函數(shù),父類的構(gòu)造函數(shù)中調(diào)用了 callName 方法,此時(shí)父類中的 baseName 的值為 base,可是子類重寫了 callName 方法,且 調(diào)用父類 Base 中的 callName 是在子類 Sub 中調(diào)用的,因此當(dāng)前的 this 指向的是子類,也就是說是實(shí)現(xiàn)子類的 callName 方法
5.調(diào)用子類的callName,打印baseName
實(shí)際上在new Sub()時(shí),實(shí)際執(zhí)行過程為:
public Sub(){
super();
baseName = "sub";
}
可見,在 baseName = "sub" 執(zhí)行前,子類的 callName() 已經(jīng)執(zhí)行,所以子類的 baseName 為默認(rèn)值狀態(tài) null 。
時(shí)間也很晚了(2016年12月27日01:35:54),最后給出一個(gè)知識(shí)點(diǎn)就睡了
構(gòu)造器的初始化順序大概是:父類靜態(tài)塊 ->子類靜態(tài)塊 ->父類初始化語句 ->父類構(gòu)造函器 ->子類初始化語句 子類構(gòu)造器。