# 7.7 構造器和多態性
同往常一樣,構造器與其他種類的方法是有區別的。在涉及到多態性的問題后,這種方法依然成立。盡管構造器并不具有多態性(即便可以使用一種“虛擬構造器”——將在第11章介紹),但仍然非常有必要理解構造器如何在復雜的分級結構中以及隨同多態性使用。這一理解將有助于大家避免陷入一些令人不快的糾紛。
## 7.7.1 構造器的調用順序
構造器調用的順序已在第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`),由于我們應盡可能地對它們進行初始化,所以也應保證構造器內部的所有成員均為有效。若堅持按這一規則行事,會有助于我們確定所有基類成員以及當前對象的成員對象均已獲得正確的初始化。但不幸的是,這種做法并不適用于所有情況,這將在下一節具體說明。
## 7.7.2 繼承和`finalize()`
通過“組合”方法創建新類時,永遠不必擔心對那個類的成員對象的收尾工作。每個成員都是一個獨立的對象,所以會得到正常的垃圾收集以及收尾處理——無論它是不是不自己某個類一個成員。但在進行初始化的時候,必須覆蓋派生類中的`finalize()`方法——如果已經設計了某個特殊的清除進程,要求它必須作為垃圾收集的一部分進行。覆蓋派生類的`finalize()`時,務必記住調用`finalize()`的基類版本。否則,基類的初始化根本不會發生。下面這個例子便是明證:
```
//: Frog.java
// Testing finalize with inheritance
class DoBaseFinalization {
public static boolean flag = false;
}
class Characteristic {
String s;
Characteristic(String c) {
s = c;
System.out.println(
"Creating Characteristic " + s);
}
protected void finalize() {
System.out.println(
"finalizing Characteristic " + s);
}
}
class LivingCreature {
Characteristic p =
new Characteristic("is alive");
LivingCreature() {
System.out.println("LivingCreature()");
}
protected void finalize() {
System.out.println(
"LivingCreature finalize");
// Call base-class version LAST!
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
}
class Animal extends LivingCreature {
Characteristic p =
new Characteristic("has heart");
Animal() {
System.out.println("Animal()");
}
protected void finalize() {
System.out.println("Animal finalize");
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
}
class Amphibian extends Animal {
Characteristic p =
new Characteristic("can live in water");
Amphibian() {
System.out.println("Amphibian()");
}
protected void finalize() {
System.out.println("Amphibian finalize");
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
}
public class Frog extends Amphibian {
Frog() {
System.out.println("Frog()");
}
protected void finalize() {
System.out.println("Frog finalize");
if(DoBaseFinalization.flag)
try {
super.finalize();
} catch(Throwable t) {}
}
public static void main(String[] args) {
if(args.length != 0 &&
args[0].equals("finalize"))
DoBaseFinalization.flag = true;
else
System.out.println("not finalizing bases");
new Frog(); // Instantly becomes garbage
System.out.println("bye!");
// Must do this to guarantee that all
// finalizers will be called:
System.runFinalizersOnExit(true);
}
} ///:~
```
`DoBasefinalization`類只是簡單地容納了一個標志,向分級結構中的每個類指出是否應調用`super.finalize()`。這個標志的設置建立在命令行參數的基礎上,所以能夠在進行和不進行基類收尾工作的前提下查看行為。
分級結構中的每個類也包含了`Characteristic`類的一個成員對象。大家可以看到,無論是否調用了基類收尾模塊,`Characteristi`c成員對象都肯定會得到收尾(清除)處理。
每個被覆蓋的`finalize()`至少要擁有對`protected`成員的訪問權力,因為`Object`類中的`finalize()`方法具有`protected`屬性,而編譯器不允許我們在繼承過程中消除訪問權限(“友好的”比“受到保護的”具有更小的訪問權限)。
在`Frog.main()`中,`DoBaseFinalization`標志會得到配置,而且會創建單獨一個`Frog`對象。請記住垃圾收集(特別是收尾工作)可能不會針對任何特定的對象發生,所以為了強制采取這一行動,`System.runFinalizersOnExit(true)`添加了額外的開銷,以保證收尾工作的正常進行。若沒有基類初始化,則輸出結果是:
```
not finalizing bases
Creating Characteristic is alive
LivingCreature()
Creating Characteristic has heart
Animal()
Creating Characteristic can live in water
Amphibian()
Frog()
bye!
Frog finalize
finalizing Characteristic is alive
finalizing Characteristic has heart
finalizing Characteristic can live in water
```
從中可以看出確實沒有為基類·調用收尾模塊。但假如在命令行加入`finalize`參數,則會獲得下述結果:
```
Creating Characteristic is alive
LivingCreature()
Creating Characteristic has heart
Animal()
Creating Characteristic can live in water
Amphibian()
Frog()
bye!
Frog finalize
Amphibian finalize
Animal finalize
LivingCreature finalize
finalizing Characteristic is alive
finalizing Characteristic has heart
finalizing Characteristic can live in water
```
盡管成員對象按照與它們創建時相同的順序進行收尾,但從技術角度說,并沒有指定對象收尾的順序。但對于基類,我們可對收尾的順序進行控制。采用的最佳順序正是在這里采用的順序,它與初始化順序正好相反。按照與C++中用于“析構器”相同的形式,我們應該首先執行派生類的收尾,再是基類的收尾。這是由于派生類的收尾可能調用基類中相同的方法,要求基類組件仍然處于活動狀態。因此,必須提前將它們清除(析構)。
## 7.7.3 構造器內部的多態性方法的行為
構造器調用的分級結構(順序)為我們帶來了一個有趣的問題,或者說讓我們進入了一種進退兩難的局面。若當前位于一個構造器的內部,同時調用準備構建的那個對象的一個動態綁定方法,那么會出現什么情況呢?在原始的方法內部,我們完全可以想象會發生什么——動態綁定的調用會在運行期間進行解析,因為對象不知道它到底從屬于方法所在的那個類,還是從屬于從它派生出來的某些類。為保持一致性,大家也許會認為這應該在構造器內部發生。
但實際情況并非完全如此。若調用構造器內部一個動態綁定的方法,會使用那個方法被覆蓋的定義。然而,產生的效果可能并不如我們所愿,而且可能造成一些難于發現的程序錯誤。
從概念上講,構造器的職責是讓對象實際進入存在狀態。在任何構造器內部,整個對象可能只是得到部分組織——我們只知道基類對象已得到初始化,但卻不知道哪些類已經繼承。然而,一個動態綁定的方法調用卻會在分級結構里“向前”或者“向外”前進。它調用位于派生類里的一個方法。如果在構造器內部做這件事情,那么對于調用的方法,它要操縱的成員可能尚未得到正確的初始化——這顯然不是我們所希望的。
通過觀察下面這個例子,這個問題便會昭然若揭:
```
//: PolyConstructors.java
// Constructors and polymorphism
// don't produce what you might expect.
abstract class Glyph {
abstract void draw();
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println(
"RoundGlyph.RoundGlyph(), radius = "
+ radius);
}
void draw() {
System.out.println(
"RoundGlyph.draw(), radius = " + radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
} ///:~
```
在`Glyph`中,`draw()`方法是“抽象的”(`abstract`),所以它可以被其他方法覆蓋。事實上,我們在`RoundGlyph`中不得不對其進行覆蓋。但`Glyph`構造器會調用這個方法,而且調用會在`RoundGlyph.draw()`中止,這看起來似乎是有意的。但請看看輸出結果:
```
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
```
當`Glyph`的構造器調用`draw()`時,`radius`的值甚至不是默認的初始值1,而是0。這可能是由于一個點號或者屏幕上根本什么都沒有畫而造成的。這樣就不得不開始查找程序中的錯誤,試著找出程序不能工作的原因。
前一節講述的初始化順序并不十分完整,而那是解決問題的關鍵所在。初始化的實際過程是這樣的:
(1) 在采取其他任何操作之前,為對象分配的存儲空間初始化成二進制零。
(2) 就象前面敘述的那樣,調用基類構造器。此時,被覆蓋的`draw()`方法會得到調用(的確是在`RoundGlyph`構造器調用之前),此時會發現`radius`的值為0,這是由于步驟(1)造成的。
(3) 按照原先聲明的順序調用成員初始化代碼。
(4) 調用派生類構造器的主體。
采取這些操作要求有一個前提,那就是所有東西都至少要初始化成零(或者某些特殊數據類型與“零”等價的值),而不是僅僅留作垃圾。其中包括通過“組合”技術嵌入一個類內部的對象引用。如果假若忘記初始化那個引用,就會在運行期間出現異常事件。其他所有東西都會變成零,這在觀看結果時通常是一個嚴重的警告信號。
在另一方面,應對這個程序的結果提高警惕。從邏輯的角度說,我們似乎已進行了無懈可擊的設計,所以它的錯誤行為令人非常不可思議。而且沒有從編譯器那里收到任何報錯信息(C++在這種情況下會表現出更合理的行為)。象這樣的錯誤會很輕易地被人忽略,而且要花很長的時間才能找出。
因此,設計構造器時一個特別有效的規則是:用盡可能簡單的方法使對象進入就緒狀態;如果可能,避免調用任何方法。在構造器內唯一能夠安全調用的是在基類中具有`final`屬性的那些方法(也適用于`private`方法,它們自動具有`final`屬性)。這些方法不能被覆蓋,所以不會出現上述潛在的問題。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物