# 12.3 克隆的控制
為消除克隆能力,大家也許認為只需將`clone()`方法簡單地設為`private`(私有)即可,但這樣是行不通的,因為不能采用一個基類方法,并使其在派生類中更“私有”。所以事情并沒有這么簡單。此外,我們有必要控制一個對象是否能夠克隆。對于我們設計的一個類,實際有許多種方案都是可以采取的:
(1) 保持中立,不為克隆做任何事情。也就是說,盡管不可對我們的類克隆,但從它繼承的一個類卻可根據實際情況決定克隆。只有`Object.clone()`要對類中的字段進行某些合理的操作時,才可以作這方面的決定。
(2) 支持`clone()`,采用實現`Cloneable`(可克隆)能力的標準操作,并覆蓋`clone()`。在被覆蓋的`clone()`中,可調用`super.clone()`,并捕獲所有異常(這樣可使`clone()`不“拋”出任何異常)。
(3) 有條件地支持克隆。若類容納了其他對象的引用,而那些對象也許能夠克隆(集合類便是這樣的一個例子),就可試著克隆擁有對方引用的所有對象;如果它們“拋”出了異常,只需讓這些異常通過即可。舉個例子來說,假設有一個特殊的`Vector`,它試圖克隆自己容納的所有對象。編寫這樣的一個`Vector`時,并不知道客戶程序員會把什么形式的對象置入這個`Vector`中,所以并不知道它們是否真的能夠克隆。
(4) 不實現`Cloneable()`,但是將`clone()`覆蓋成`protected`,使任何字段都具有正確的復制行為。這樣一來,從這個類繼承的所有東西都能覆蓋`clone()`,并調用`super.clone()`來產生正確的復制行為。注意在我們實現方案里,可以而且應該調用`super.clone()`——即使那個方法本來預期的是一個`Cloneable`對象(否則會拋出一個異常),因為沒有人會在我們這種類型的對象上直接調用它。它只有通過一個派生類調用;對那個派生類來說,如果要保證它正常工作,需實現`Cloneable`。
(5) 不實現`Cloneable`來試著防止克隆,并覆蓋`clone()`,以產生一個異常。為使這一設想順利實現,只有令從它派生出來的任何類都調用重新定義后的`clone()`里的`suepr.clone()`。
(6) 將類設為`final`,從而防止克隆。若`clone()`尚未被我們的任何一個上級類覆蓋,這一設想便不會成功。若已被覆蓋,那么再一次覆蓋它,并“拋”出一個`CloneNotSupportedException`(克隆不支持)異常。為擔保克隆被禁止,將類設為`final`是唯一的辦法。除此以外,一旦涉及保密對象或者遇到想對創建的對象數量進行控制的其他情況,應該將所有構造器都設為`private`,并提供一個或更多的特殊方法來創建對象。采用這種方式,這些方法就可以限制創建的對象數量以及它們的創建條件——一種特殊情況是第16章要介紹的singleton(單例)方案。
下面這個例子總結了克隆的各種實現方法,然后在層次結構中將其“關閉”:
```
//: CheckCloneable.java
// Checking to see if a handle can be cloned
// Can't clone this because it doesn't
// override clone():
class Ordinary {}
// Overrides clone, but doesn't implement
// Cloneable:
class WrongClone extends Ordinary {
public Object clone()
throws CloneNotSupportedException {
return super.clone(); // Throws exception
}
}
// Does all the right things for cloning:
class IsCloneable extends Ordinary
implements Cloneable {
public Object clone()
throws CloneNotSupportedException {
return super.clone();
}
}
// Turn off cloning by throwing the exception:
class NoMore extends IsCloneable {
public Object clone()
throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}
class TryMore extends NoMore {
public Object clone()
throws CloneNotSupportedException {
// Calls NoMore.clone(), throws exception:
return super.clone();
}
}
class BackOn extends NoMore {
private BackOn duplicate(BackOn b) {
// Somehow make a copy of b
// and return that copy. This is a dummy
// copy, just to make the point:
return new BackOn();
}
public Object clone() {
// Doesn't call NoMore.clone():
return duplicate(this);
}
}
// Can't inherit from this, so can't override
// the clone method like in BackOn:
final class ReallyNoMore extends NoMore {}
public class CheckCloneable {
static Ordinary tryToClone(Ordinary ord) {
String id = ord.getClass().getName();
Ordinary x = null;
if(ord instanceof Cloneable) {
try {
System.out.println("Attempting " + id);
x = (Ordinary)((IsCloneable)ord).clone();
System.out.println("Cloned " + id);
} catch(CloneNotSupportedException e) {
System.out.println(
"Could not clone " + id);
}
}
return x;
}
public static void main(String[] args) {
// Upcasting:
Ordinary[] ord = {
new IsCloneable(),
new WrongClone(),
new NoMore(),
new TryMore(),
new BackOn(),
new ReallyNoMore(),
};
Ordinary x = new Ordinary();
// This won't compile, since clone() is
// protected in Object:
//! x = (Ordinary)x.clone();
// tryToClone() checks first to see if
// a class implements Cloneable:
for(int i = 0; i < ord.length; i++)
tryToClone(ord[i]);
}
} ///:~
```
第一個類`Ordinary`代表著大家在本書各處最常見到的類:不支持克隆,但在它正式應用以后,卻也不禁止對其克隆。但假如有一個指向`Ordinary`對象的引用,而且那個對象可能是從一個更深的派生類向上轉換來的,便不能判斷它到底能不能克隆。
`WrongClone`類揭示了實現克隆的一種不正確途徑。它確實覆蓋了`Object.clone()`,并將那個方法設為`public`,但卻沒有實現`Cloneable`。所以一旦發出對`super.clone()`的調用(由于對`Object.clone()`的一個調用造成的),便會無情地拋出`CloneNotSupportedException`異常。
在`IsCloneable`中,大家看到的才是進行克隆的各種正確行動:先覆蓋`clone()`,并實現了`Cloneable`。但是,這個`clone()`方法以及本例的另外幾個方法并不捕獲`CloneNotSupportedException`異常,而是任由它通過,并傳遞給調用者。隨后,調用者必須用一個`try-catch`代碼塊把它包圍起來。在我們自己的`clone()`方法中,通常需要在`clone()`內部捕獲`CloneNotSupportedException`異常,而不是任由它通過。正如大家以后會理解的那樣,對這個例子來說,讓它通過是最正確的做法。
類`NoMore`試圖按照Java設計者打算的那樣“關閉”克隆:在派生類`clone()`中,我們拋出`CloneNotSupportedException`異常。`TryMore`類中的`clone()`方法正確地調用`super.clone()`,并解析成`NoMore.clone()`,后者拋出一個異常并禁止克隆。
但在已被覆蓋的`clone()`方法中,假若程序員不遵守調用`super.clone()`的“正確”方法,又會出現什么情況呢?在`BackOn`中,大家可看到實際會發生什么。這個類用一個獨立的方法`duplicate()`制作當前對象的一個副本,并在`clone()`內部調用這個方法,而不是調用`super.clone()`。異常永遠不會產生,而且新類是可以克隆的。因此,我們不能依賴“拋”出一個異常的方法來防止產生一個可克隆的類。唯一安全的方法在`ReallyNoMore`中得到了演示,它設為`final`,所以不可繼承。這意味著假如`clone(`)在`final`類中拋出了一個異常,便不能通過繼承來進行修改,并可有效地禁止克隆(不能從一個擁有任意繼承級數的類中明確調用`Object.clone()`;只能調用`super.clone()`,它只可訪問直接基類)。因此,只要制作一些涉及安全問題的對象,就最好把那些類設為`final`。
在類`CheckCloneable`中,我們看到的第一個類是`tryToClone()`,它能接納任何`Ordinary`對象,并用`instanceof`檢查它是否能夠克隆。若答案是肯定的,就將對象轉換成為一個`IsCloneable`,調用`clone()`,并將結果轉換回`Ordinary`,最后捕獲有可能產生的任何異常。請注意用運行期類型識別(見第11章)打印出類名,使自己看到發生的一切情況。
在`main()`中,我們創建了不同類型的`Ordinary`對象,并在數組定義中向上轉換成為`Ordinary`。在這之后的頭兩行代碼創建了一個純粹的`Ordinary`對象,并試圖對其克隆。然而,這些代碼不會得到編譯,因為`clone()`是`Object`中的一個`protected`(受到保護的)方法。代碼剩余的部分將遍歷數組,并試著克隆每個對象,分別報告它們的成功或失敗。輸出如下:
```
Attempting IsCloneable
Cloned IsCloneable
Attempting NoMore
Could not clone NoMore
Attempting TryMore
Could not clone TryMore
Attempting BackOn
Cloned BackOn
Attempting ReallyNoMore
Could not clone ReallyNoMore
```
總之,如果希望一個類能夠克隆,那么:
(1) 實現`Cloneable`接口
(2) 覆蓋`clone()`
(3) 在自己的`clone()`中調用`super.clone()`
(4) 在自己的`clone()`中捕獲異常
這一系列步驟能達到最理想的效果。
## 12.3.1 副本構造器
克隆看起來要求進行非常復雜的設置,似乎還該有另一種替代方案。一個辦法是制作特殊的構造器,令其負責復制一個對象。在C++中,這叫作“副本構造器”。剛開始的時候,這好象是一種非常顯然的解決方案(如果你是C++程序員,這個方法就更顯親切)。下面是一個實際的例子:
```
//: CopyConstructor.java
// A constructor for copying an object
// of the same type, as an attempt to create
// a local copy.
class FruitQualities {
private int weight;
private int color;
private int firmness;
private int ripeness;
private int smell;
// etc.
FruitQualities() { // Default constructor
// do something meaningful...
}
// Other constructors:
// ...
// Copy constructor:
FruitQualities(FruitQualities f) {
weight = f.weight;
color = f.color;
firmness = f.firmness;
ripeness = f.ripeness;
smell = f.smell;
// etc.
}
}
class Seed {
// Members...
Seed() { /* Default constructor */ }
Seed(Seed s) { /* Copy constructor */ }
}
class Fruit {
private FruitQualities fq;
private int seeds;
private Seed[] s;
Fruit(FruitQualities q, int seedCount) {
fq = q;
seeds = seedCount;
s = new Seed[seeds];
for(int i = 0; i < seeds; i++)
s[i] = new Seed();
}
// Other constructors:
// ...
// Copy constructor:
Fruit(Fruit f) {
fq = new FruitQualities(f.fq);
seeds = f.seeds;
// Call all Seed copy-constructors:
for(int i = 0; i < seeds; i++)
s[i] = new Seed(f.s[i]);
// Other copy-construction activities...
}
// To allow derived constructors (or other
// methods) to put in different qualities:
protected void addQualities(FruitQualities q) {
fq = q;
}
protected FruitQualities getQualities() {
return fq;
}
}
class Tomato extends Fruit {
Tomato() {
super(new FruitQualities(), 100);
}
Tomato(Tomato t) { // Copy-constructor
super(t); // Upcast for base copy-constructor
// Other copy-construction activities...
}
}
class ZebraQualities extends FruitQualities {
private int stripedness;
ZebraQualities() { // Default constructor
// do something meaningful...
}
ZebraQualities(ZebraQualities z) {
super(z);
stripedness = z.stripedness;
}
}
class GreenZebra extends Tomato {
GreenZebra() {
addQualities(new ZebraQualities());
}
GreenZebra(GreenZebra g) {
super(g); // Calls Tomato(Tomato)
// Restore the right qualities:
addQualities(new ZebraQualities());
}
void evaluate() {
ZebraQualities zq =
(ZebraQualities)getQualities();
// Do something with the qualities
// ...
}
}
public class CopyConstructor {
public static void ripen(Tomato t) {
// Use the "copy constructor":
t = new Tomato(t);
System.out.println("In ripen, t is a " +
t.getClass().getName());
}
public static void slice(Fruit f) {
f = new Fruit(f); // Hmmm... will this work?
System.out.println("In slice, f is a " +
f.getClass().getName());
}
public static void main(String[] args) {
Tomato tomato = new Tomato();
ripen(tomato); // OK
slice(tomato); // OOPS!
GreenZebra g = new GreenZebra();
ripen(g); // OOPS!
slice(g); // OOPS!
g.evaluate();
}
} ///:~
```
這個例子第一眼看上去顯得有點奇怪。不同水果的質量肯定有所區別,但為什么只是把代表那些質量的數據成員直接置入`Fruit`(水果)類?有兩方面可能的原因。第一個是我們可能想簡便地插入或修改質量。注意`Fruit`有一個`protected`(受到保護的)`addQualities()`方法,它允許派生類來進行這些插入或修改操作(大家或許會認為最合乎邏輯的做法是在`Fruit`中使用一個`protected`構造器,用它獲取`FruitQualities`參數,但構造器不能繼承,所以不可在第二級或級數更深的類中使用它)。通過將水果的質量置入一個獨立的類,可以得到更大的靈活性,其中包括可以在特定`Fruit`對象的存在期間中途更改質量。
之所以將`FruitQualities`設為一個獨立的對象,另一個原因是考慮到我們有時希望添加新的質量,或者通過繼承與多態性改變行為。注意對`GreenZebra`來說(這實際是西紅柿的一類——我已栽種成功,它們簡直令人難以置信),構造器會調用`addQualities()`,并為其傳遞一個`ZebraQualities`對象。該對象是從`FruitQualities`派生出來的,所以能與基類中的`FruitQualities`引用聯系在一起。當然,一旦`GreenZebr`a使用`FruitQualities`,就必須將其向下轉換成為正確的類型(就象`evaluate()`中展示的那樣),但它肯定知道類型是`ZebraQualities`。
大家也看到有一個`Seed`(種子)類,`Fruit`(大家都知道,水果含有自己的種子)包含了一個`Seed`數組。
最后,注意每個類都有一個副本構造器,而且每個副本構造器都必須關心為基類和成員對象調用副本構造器的問題,從而獲得“深層復制”的效果。對副本構造器的測試是在`CopyConstructor`類內進行的。方法`ripen()`需要獲取一個`Tomato`參數,并對其執行副本構建工作,以便復制對象:
```
t = new Tomato(t);
```
而`slice()`需要獲取一個更常規的`Fruit`對象,而且對它進行復制:
```
f = new Fruit(f);
```
它們都在`main()`中伴隨不同種類的`Fruit`進行測試。下面是輸出結果:
```
In ripen, t is a Tomato
In slice, f is a Fruit
In ripen, t is a Tomato
In slice, f is a Fruit
```
從中可以看出一個問題。在`slice()`內部對`Tomato`進行了副本構建工作以后,結果便不再是一個`Tomato`對象,而只是一個`Fruit`。它已丟失了作為一個`Tomato`(西紅柿)的所有特征。此外,如果采用一個`GreenZebra`,`ripen()`和`slice()`會把它分別轉換成一個`Tomato`和一個`Fruit`。所以非常不幸,假如想制作對象的一個本地副本,Java中的副本構造器便不是特別適合我們。
(1) 為什么在C++的作用比在Java中大?
副本構造器是C++的一個基本構成部分,因為它能自動產生對象的一個本地副本。但前面的例子確實證明了它不適合在Java中使用,為什么呢?在Java中,我們操控的一切東西都是引用,而在C++中,卻可以使用類似于引用的東西,也能直接傳遞對象。這時便要用到C++的副本構造器:只要想獲得一個對象,并按值傳遞它,就可以復制對象。所以它在C++里能很好地工作,但應注意這套機制在Java里是很不通的,所以不要用它。
- 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 推薦讀物