構建器調用的順序已在第4章進行了簡要說明,但那是在繼承和多形性問題引入之前說的話。
用於基礎類的構建器肯定在一個衍生類的構建器中調用,而且逐漸向上鏈接,使每個基礎類使用的構建器都能得到調用。之所以要這樣做,是由於構建器負有一項特殊任務:檢查對象是否得到了正確的構建。一個衍生類只能訪問它自己的成員,不能訪問基礎類的成員(這些成員通常都具有private屬性)。只有基礎類的構建器在初始化自己的元素時才知道正確的方法以及擁有適當的權限。所以,必須令所有構建器都得到調用,否則整個對象的構建就可能不正確。那正是編譯器為什麼要強迫對衍生類的每個部分進行構建器調用的原因。在衍生類的構建器主體中,若我們沒有明確指定對一個基礎類構建器的調用,它就會“默默”地調用默認構建器。如果不存在默認構建器,編譯器就會報告一個錯誤(若某個類沒有構建器,編譯器會自動組織一個默認構建器)。
下面讓我們看看一個例子,它展示了按構建順序進行合成、繼承以及多形性的效果:
//: Sandwich.java // Order of constructor calls class Meal { Meal() { System.out.println("Meal()"); } } class Bread { Bread() { System.out.println("Bread()"); } } class Cheese { Cheese() { System.out.println("Cheese()"); } } class Lettuce { Lettuce() { System.out.println("Lettuce()"); } } class Lunch extends Meal { Lunch() { System.out.println("Lunch()");} } class PortableLunch extends Lunch { PortableLunch() { System.out.println("PortableLunch()"); } } class Sandwich extends PortableLunch { Bread b = new Bread(); Cheese c = new Cheese(); Lettuce l = new Lettuce(); Sandwich() { System.out.println("Sandwich()"); } public static void main(String[] args) { new Sandwich(); } } ///:~
這個例子在其他類的外部創建了一個復雜的類,而且每個類都有一個構建器對自己進行了宣布。其中最重要的類是Sandwich,它反映出了三個級別的繼承(若將從Object的默認繼承算在內,就是四級)以及三個成員對象。在main()裡創建了一個Sandwich對象後,輸出結果如下:
Meal() Lunch() PortableLunch() Bread() Cheese() Lettuce() Sandwich()
這意味著對於一個復雜的對象,構建器的調用遵照下面的順序:
(1) 調用基礎類構建器。這個步驟會不斷重復下去,首先得到構建的是分級結構的根部,然後是下一個衍生類,等等。直到抵達最深一層的衍生類。
(2) 按聲明順序調用成員初始化模塊。
(3) 調用衍生構建器的主體。
構建器調用的順序是非常重要的。進行繼承時,我們知道關於基礎類的一切,並且能訪問基礎類的任何public和protected成員。這意味著當我們在衍生類的時候,必須能假定基礎類的所有成員都是有效的。采用一種標准方法,構建行動已經進行,所以對象所有部分的成員均已得到構建。但在構建器內部,必須保證使用的所有成員都已構建。為達到這個要求,唯一的辦法就是首先調用基礎類構建器。然後在進入衍生類構建器以後,我們在基礎類能夠訪問的所有成員都已得到初始化。此外,所有成員對象(亦即通過合成方法置於類內的對象)在類內進行定義的時候(比如上例中的b,c和l),由於我們應盡可能地對它們進行初始化,所以也應保證構建器內部的所有成員均為有效。若堅持按這一規則行事,會有助於我們確定所有基礎類成員以及當前對象的成員對象均已獲得正確的初始化。但不幸的是,這種做法並不適用於所有情況,這將在下一節具體說明。