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

讓我們先來看兩個類: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)。

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

  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解惑》。

轉自:http://coolshell.cn/articles/1106.html

原創文章,作者:s19930811,如若轉載,請注明出處:http://www.www58058.com/2402

(0)
s19930811s19930811
上一篇 2015-04-03 21:53
下一篇 2015-04-03 21:59

相關推薦

  • 如何正確安裝一個源碼包

        下周就要考試了,心情挺忐忑不安的,前幾天做了25期的考試題,感覺每個題都是老師上課講過的,但是自己做卻想不起來了。這應該就像學習數學一樣,需要大量練習,做的多了自然就會了。     這周我們學習了如何使用yum,還學習了磁盤管理。針對如何安裝源碼包,以httpd包為例我們…

    2017-08-19
  • 計算機網絡基礎及常用工具

    Linux網絡屬性配置      計算機網絡:      TCP/IP: 協議棧(使用)      ISO, OSI: 協議棧(學習)  MAC:Media Access Control      48bits:   &…

    Linux干貨 2017-01-02
  • linux三劍客之awk

    awk             簡介:是一個優良的文本處理工具,Linux及Unix環境中現有的功能最強大的數據處理引擎之一。這種編程及數據操作語言的最大功能         &n…

    Linux干貨 2016-12-05
  • iptables總結

    iptables簡稱為包過濾型防火墻一般分為2種:     1,主機防火墻:主機防火墻是用來防止本主機內的應用服務被攻擊所需要保護的防火墻     2,網絡防火墻:做為想路由功能的防火墻凡是進過此服務器的數據包都要進行規則匹配 iptables的架構由功能和鏈組成  &…

    Linux干貨 2016-12-05
  • Linux計劃任務

    計劃任務 工作當中有時候需要將某件事情在未來的某個特定的時間執行,而自己確不在機器旁,該如何是好?像這樣在每天特定的時間內去安排做一些事情這樣,一種事情我們就稱之為例行性的計劃任務,其實在我們的操作系統當中都有類似的例行性任務計劃功能,那如何去像設定鬧鐘一種在Linux上指定例行性計劃任務并實施,主要有兩種工具:at和crontab   計劃任務分…

    Linux干貨 2016-09-19
  • Linux文件類型及顏色標識

    文件類型(共7種): – :普通文件 d:目錄文件 (directory) c:字符設備文件 (char) b:塊設備文件 (block) s:本地域套接口 (socket) p:有名管道 (pipeline) l:符號連接 (link) 關于硬鏈接、軟連接、復制之間的區別說明: 上圖中,我為photo.png這個圖片文件建立了一個拷貝(phot…

    Linux干貨 2016-10-16
欧美性久久久久