原文:http://cocre.com/?p=1106 酷殼
讓我們先來看兩個類:Base和Derived類。注意其中的whenAmISet成員變數,和方法preProcess()
1.
public
class
Base
2.
{
3.
Base() {
4.
preProcess();
5.
}
6.
7.
void
preProcess() {}
8.
}
01.
public
class
Derived
extends
Base
02.
{
03.
public
String whenAmISet =
"set when declared"
;
04.
05.
@Override
void
preProcess()
06.
{
07.
whenAmISet =
"set in preProcess()"
;
08.
}
09.
}
如果我們構造一個子類執行個體,那麼,whenAmISet 的值會是什麼呢?
1.
public
class
Main
2.
{
3.
public
static
void
main(String[] args)
4.
{
5.
Derived d =
new
Derived();
6.
System.out.println( d.whenAmISet );
7.
}
8.
}
再續繼往下閱讀之前,請先給自己一些時間想一下上面的這段程式的輸出是什嗎?是的,這看起來的確相當簡單,甚至不需要編譯和運行上面的代碼,我們也應該知道其答案,那麼,你覺得你知道答案嗎?你確定你的答案正確嗎?
很多人都會覺得那段程式的輸出應該是“set in preProcess()”,這是因為當子類Derived 的建構函式被調用時,其會隱晦地調用其基類Base的建構函式(通過super()函數),於是基類Base的建構函式會調用preProcess() 函數,因為這個類的執行個體是Derived的,而且在子類Derived中對這個函數使用了override關鍵字,所以,實際上調用到的是:Derived.preProcess(),而這個方法設定了whenAmISet 成員變數的值為:“set in preProcess()”。
當然,上面的結論是錯誤的。如果你編譯並運行這個程式,你會發現,程式實際輸出的是“set when declared ”。怎麼為這樣呢?難道是基類Base 的preProcess() 方法被調用啦?也不是!你可以在基類的preProcess中輸出點什麼看看,你會發現程式運行時,Base.preProcess()並沒有被調用到(不然這對於Java所有的應用程式將會是一個極具災難性的Bug)。
雖然上面的結論是錯誤的,但推導過程是合理的,只是不完整,下面是整個啟動並執行流程:
- 進入Derived 建構函式。
- Derived 成員變數的記憶體被分配。
- Base 建構函式被隱含調用。
- Base 建構函式調用preProcess()。
- Derived 的preProcess 設定whenAmISet 值為 “set in preProcess()”。
- Derived 的成員變數初始化被調用。
- 執行Derived 建構函式體。
等一等,這怎麼可能?在第6步,Derived 成員的初始化居然在 preProcess() 調用之後?是的,正是這樣,我們不能讓成員變數的聲明和初始化變成一個原子操作,雖然在Java中我們可以把其寫在一起,讓其看上去像是聲明和初始化一體。但這隻是假象,我們的錯誤就在於我們把Java中的聲明和初始化看成了一體 。在C++的世界中,C++並不支援成員變數在聲明的時候進行初始化,其需要你在建構函式中顯式的初始化其成員變數的值,看起來很土,但其實C++用心良苦。
在物件導向的世界中,因為程式以對象的形式出現,導致了我們對程式執行的順序霧裡看花。所以,在物件導向的世界中,程式執行的順序相當的重要 。
下面是對上面各個步驟的逐條解釋。
- 進入建構函式。
- 為成員變數分配記憶體。
- 除非你顯式地調用super(),否則Java 會在子類的建構函式最前面偷偷地插入super() 。
- 調用父類建構函式。
- 調用preProcess,因為被子類override,所以調用的是子類的。
- 於是,初始化發生在了preProcess()之後。這是因為,Java需要保證父類的初始化早於子類的成員初始化,否則,在子類中使用父類的成員變數就會出現問題。
- 正式執行子類的建構函式(當然這是一個空函數,居然我們沒有聲明)。
你可以查看《Java語言的規格說明書》中的 相關章節 來瞭解更多的Java建立對象時的細節。
C++的程式員應該都知道,在C++的世界中在“建構函式中調用虛函數”是不行的,Effective C++ 條款9:Never call virtual functions during construction or destruction,Scott Meyers已經解釋得很詳細了。
在語言設計的時候,“在建構函式中調用虛函數”是個兩難的問題。
- 如果調用的是父類的函數的話,這個有點違反虛函數的定義。
- 如果調用的是子類的函數的話,這可能產生問題的:因為在構造子類對象的時候,首先調用父類的建構函式,而這時候如果去調用子類的函數,由於子類還沒有構造完成,子類的成員尚未初始化,這麼做顯然是不安全的。
C++選擇了第一種,而Java選擇了第二種。
- C++類的設計相對比較簡陋,通過虛函數表來實現,缺少類的元資訊。
- 而Java類的則顯得比較完整,有super指標來導航到父類。
最後,需要向大家推薦一本書,Joshua Bloch 和 Neal Gafter 寫的 Java Puzzlers: Traps, Pitfalls, and Corner Cases ,中文版《JAVA解惑 》。
(轉載時請註明作者和出處,請勿用於商業用途 )