讓我們先來看兩個類:Base和Derived類。注意其中的whenAmISet成員變量,和方法preProcess()
public class Base { Base() { preProcess(); } void preProcess() {} }
public class Derived extends Base { public String whenAmISet = "set when declared"; @Override void preProcess() { whenAmISet = "set in preProcess()"; } }
如果我們構造一個子類實例,那麼,whenAmISet 的值會是什麼呢?
public class Main { public static void main(String[] args) { Derived d = new Derived(); System.out.println( d.whenAmISet ); } }
再續繼往下閱讀之前,請先給自己一些時間想一下上面的這段程序的輸出是什麼?是的,這看起來的確相當簡單,甚至不需要編譯和運行上面的代碼,我們也應該知道其答案,那麼,你覺得你知道答案嗎?你確定你的答案正確嗎?
很多人都會覺得那段程序的輸出應該是“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)。
雖然上面的結論是錯誤的,但推導過程是合理的,只是不完整,下面是整個運行的流程:
等一等,這怎麼可能?在第6步,Derived 成員的初始化居然在 preProcess() 調用之後?是的,正是這樣,我們不能讓成員變量的聲明和初始化變成一個原子操作,雖然在Java中我們可以把其寫在一起,讓其看上去像是聲明和初始化一體。但這只是假象,我們的錯誤就在於我們把Java中的聲明和初始化看成了一體。在C++的世界中,C++並不支持成員變量在聲明的時候進行初始化,其需要你在構造函數中顯式的初始化其成員變量的值,看起來很土,但其實C++用心良苦。
在面向對象的世界中,因為程序以對象的形式出現,導致了我們對程序執行的順序霧裡看花。所以,在面向對象的世界中,程序執行的順序相當的重要。
下面是對上面各個步驟的逐條解釋。
你可以查看《Java語言的規格說明書》中的 相關章節 來了解更多的Java創建對象時的細節。
C++的程序員應該都知道,在C++的世界中在“構造函數中調用虛函數”是不行的,Effective C++ 條款9:Never call virtual functions during construction or destruction,Scott Meyers已經解釋得很詳細了。
在語言設計的時候,“在構造函數中調用虛函數”是個兩難的問題。
C++選擇了第一種,而Java選擇了第二種。
最後,需要向大家推薦一本書,Joshua Bloch 和 Neal Gafter 寫的 Java Puzzlers: Traps, Pitfalls, and Corner Cases,中文版《JAVA解惑》。