Java構造時成員初始化的陷阱

來源:互聯網
上載者:User

原文: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)。

雖然上面的結論是錯誤的,但推導過程是合理的,只是不完整,下面是整個啟動並執行流程:

  1. 進入Derived 建構函式。
  2. Derived 成員變數的記憶體被分配。
  3. Base 建構函式被隱含調用。
  4. Base 建構函式調用preProcess()。
  5. Derived 的preProcess 設定whenAmISet 值為 “set in preProcess()”。
  6. Derived 的成員變數初始化被調用。
  7. 執行Derived 建構函式體。

等一等,這怎麼可能?在第6步,Derived 成員的初始化居然在 preProcess() 調用之後?是的,正是這樣,我們不能讓成員變數的聲明和初始化變成一個原子操作,雖然在Java中我們可以把其寫在一起,讓其看上去像是聲明和初始化一體。但這隻是假象,我們的錯誤就在於我們把Java中的聲明和初始化看成了一體 在C++的世界中,C++並不支援成員變數在聲明的時候進行初始化,其需要你在建構函式中顯式的初始化其成員變數的值,看起來很土,但其實C++用心良苦。

在物件導向的世界中,因為程式以對象的形式出現,導致了我們對程式執行的順序霧裡看花。所以,在物件導向的世界中,程式執行的順序相當的重要

下面是對上面各個步驟的逐條解釋。

  1. 進入建構函式。
  2. 為成員變數分配記憶體。
  3. 除非你顯式地調用super(),否則Java 會在子類的建構函式最前面偷偷地插入super() 。
  4. 調用父類建構函式。
  5. 調用preProcess,因為被子類override,所以調用的是子類的。
  6. 於是,初始化發生在了preProcess()之後。這是因為,Java需要保證父類的初始化早於子類的成員初始化,否則,在子類中使用父類的成員變數就會出現問題。
  7. 正式執行子類的建構函式(當然這是一個空函數,居然我們沒有聲明)。

你可以查看《Java語言的規格說明書》中的 相關章節 來瞭解更多的Java建立對象時的細節。

 

C++的程式員應該都知道,在C++的世界中在“建構函式中調用虛函數”是不行的,Effective C++ 條款9:Never call virtual functions during construction or destruction,Scott Meyers已經解釋得很詳細了。

在語言設計的時候,“在建構函式中調用虛函數”是個兩難的問題。

  1. 如果調用的是父類的函數的話,這個有點違反虛函數的定義。
  2. 如果調用的是子類的函數的話,這可能產生問題的:因為在構造子類對象的時候,首先調用父類的建構函式,而這時候如果去調用子類的函數,由於子類還沒有構造完成,子類的成員尚未初始化,這麼做顯然是不安全的。

C++選擇了第一種,而Java選擇了第二種。

  • C++類的設計相對比較簡陋,通過虛函數表來實現,缺少類的元資訊。
  • 而Java類的則顯得比較完整,有super指標來導航到父類。

最後,需要向大家推薦一本書,Joshua Bloch 和 Neal Gafter 寫的 Java Puzzlers: Traps, Pitfalls, and Corner Cases ,中文版《JAVA解惑 》。

 

轉載時請註明作者和出處,請勿用於商業用途

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.