# 12.2 制作本地副本
稍微總結一下:Java中的所有參數傳遞都是通過傳遞引用進行的。也就是說,當我們傳遞“一個對象”時,實際傳遞的只是指向位于方法外部的那個對象的“一個引用”。所以一旦要對那個引用進行任何修改,便相當于修改外部對象。此外:
+ 參數傳遞過程中會自動產生別名問題
+ 不存在本地對象,只有本地引用
+ 引用有自己的作用域,而對象沒有
+ 對象的“存在時間”在Java里不是個問題
+ 沒有語言上的支持(如常量)可防止對象被修改(以避免別名的副作用)
若只是從對象中讀取信息,而不修改它,傳遞引用便是參數傳遞中最有效的一種形式。這種做非常恰當;默認的方法一般也是最有效的方法。然而,有時仍需將對象當作“本地的”對待,使我們作出的改變只影響一個本地副本,不會對外面的對象造成影響。許多程序設計語言都支持在方法內自動生成外部對象的一個本地副本(注釋①)。盡管Java不具備這種能力,但允許我們達到同樣的效果。
①:在C語言中,通常控制的是少量數據位,默認操作是按值傳遞。C++也必須遵照這一形式,但按值傳遞對象并非肯定是一種有效的方式。此外,在C++中用于支持按值傳遞的代碼也較難編寫,是件讓人頭痛的事情。
## 12.2.1 按值傳遞
首先要解決術語的問題,最適合“按值傳遞”的看起來是參數。“按值傳遞”以及它的含義取決于如何理解程序的運行方式。最常見的意思是獲得要傳遞的任何東西的一個本地副本,但這里真正的問題是如何看待自己準備傳遞的東西。對于“按值傳遞”的含義,目前存在兩種存在明顯區別的見解:
(1) Java按值傳遞任何東西。若將基本數據類型傳遞進入一個方法,會明確得到基本數據類型的一個副本。但若將一個引用傳遞進入方法,得到的是引用的副本。所以人們認為“一切”都按值傳遞。當然,這種說法也有一個前提:引用肯定也會被傳遞。但Java的設計模式似乎有些超前,允許我們忽略(大多數時候)自己處理的是一個引用。也就是說,它允許我們將引用假想成“對象”,因為在發出方法調用時,系統會自動照管兩者間的差異。
(2) Java主要按值傳遞(無參數),但對象卻是按引用傳遞的。得到這個結論的前提是引用只是對象的一個“別名”,所以不考慮傳遞引用的問題,而是直接指出“我準備傳遞對象”。由于將其傳遞進入一個方法時沒有獲得對象的一個本地副本,所以對象顯然不是按值傳遞的。Sun公司似乎在某種程度上支持這一見解,因為它“保留但未實現”的關鍵字之一便是`byvalue`(按值)。但沒人知道那個關鍵字什么時候可以發揮作用。
盡管存在兩種不同的見解,但其間的分歧歸根到底是由于對“引用”的不同解釋造成的。我打算在本書剩下的部分里回避這個問題。大家不久就會知道,這個問題爭論下去其實是沒有意義的——最重要的是理解一個引用的傳遞會使調用者的對象發生意外的改變。
## 12.2.2 克隆對象
若需修改一個對象,同時不想改變調用者的對象,就要制作該對象的一個本地副本。這也是本地副本最常見的一種用途。若決定制作一個本地副本,只需簡單地使用`clone()`方法即可。`Clone`是“克隆”的意思,即制作完全一模一樣的副本。這個方法在基類`Object`中定義成`protected`(受保護)模式。但在希望克隆的任何派生類中,必須將其覆蓋為`public`模式。例如,標準庫類`Vector`覆蓋了`clone()`,所以能為`Vector`調用`clone()`,如下所示:
```
//: Cloning.java
// The clone() operation works for only a few
// items in the standard Java library.
import java.util.*;
class Int {
private int i;
public Int(int ii) { i = ii; }
public void increment() { i++; }
public String toString() {
return Integer.toString(i);
}
}
public class Cloning {
public static void main(String[] args) {
Vector v = new Vector();
for(int i = 0; i < 10; i++ )
v.addElement(new Int(i));
System.out.println("v: " + v);
Vector v2 = (Vector)v.clone();
// Increment all v2's elements:
for(Enumeration e = v2.elements();
e.hasMoreElements(); )
((Int)e.nextElement()).increment();
// See if it changed v's elements:
System.out.println("v: " + v);
}
} ///:~
```
`clone()`方法產生了一個`Object`,后者必須立即重新轉換為正確類型。這個例子指出`Vector`的`clone()`方法不能自動嘗試克隆`Vector`內包含的每個對象——由于別名問題,老的`Vector`和克隆的`Vector`都包含了相同的對象。我們通常把這種情況叫作“簡單復制”或者“淺層復制”,因為它只復制了一個對象的“表面”部分。實際對象除包含這個“表面”以外,還包括引用指向的所有對象,以及那些對象又指向的其他所有對象,由此類推。這便是“對象網”或“對象關系網”的由來。若能復制下所有這張網,便叫作“全面復制”或者“深層復制”。
在輸出中可看到淺層復制的結果,注意對`v2`采取的行動也會影響到`v`:
```
v: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
v: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
```
一般來說,由于不敢保證`Vector`里包含的對象是“可以克隆”(注釋②)的,所以最好不要試圖克隆那些對象。
②:“可以克隆”用英語講是`cloneable`,請留意Java庫中專門保留了這樣的一個關鍵字。
## 12.2.3 使類具有克隆能力
盡管克隆方法是在所有類最基本的`Object`中定義的,但克隆仍然不會在每個類里自動進行。這似乎有些不可思議,因為基類方法在派生類里是肯定能用的。但Java確實有點兒反其道而行之;如果想在一個類里使用克隆方法,唯一的辦法就是專門添加一些代碼,以便保證克隆的正常進行。
(1) 使用`protected`時的技巧
為避免我們創建的每個類都默認具有克隆能力,`clone()`方法在基類`Object`里得到了“保留”(設為`protected`)。這樣造成的后果就是:對那些簡單地使用一下這個類的客戶程序員來說,他們不會默認地擁有這個方法;其次,我們不能利用指向基類的一個引用來調用`clone()`(盡管那樣做在某些情況下特別有用,比如用多態性的方式克隆一系列對象)。在編譯期的時候,這實際是通知我們對象不可克隆的一種方式——而且最奇怪的是,Java庫中的大多數類都不能克隆。因此,假如我們執行下述代碼:
```
Integer x = new Integer(l);
x = x.clone();
```
那么在編譯期,就有一條討厭的錯誤消息彈出,告訴我們不可訪問`clone()`——因為`Integer`并沒有覆蓋它,而且它對`protected`版本來說是默認的)。
但是,假若我們是在一個從`Object`派生出來的類中(所有類都是從`Object`派生的),就有權調用`Object.clone()`,因為它是`protected`,而且我們在一個迭代器中。基類`clone()`提供了一個有用的功能——它進行的是對派生類對象的真正“按位”復制,所以相當于標準的克隆行動。然而,我們隨后需要將自己的克隆操作設為`public`,否則無法訪問。總之,克隆時要注意的兩個關鍵問題是:幾乎肯定要調用`super.clone()`,以及注意將克隆設為`public`。
有時還想在更深層的派生類中覆蓋`clone()`,否則就直接使用我們的`clone()`(現在已成為`public`),而那并不一定是我們所希望的(然而,由于`Object.clone()`已制作了實際對象的一個副本,所以也有可能允許這種情況)。`protected`的技巧在這里只能用一次:首次從一個不具備克隆能力的類繼承,而且想使一個類變成“能夠克隆”。而在從我們的類繼承的任何場合,`clone()`方法都是可以使用的,因為Java不可能在派生之后反而縮小方法的訪問范圍。換言之,一旦對象變得可以克隆,從它派生的任何東西都是能夠克隆的,除非使用特殊的機制(后面討論)令其“關閉”克隆能力。
(2) 實現`Cloneable`接口
為使一個對象的克隆能力功成圓滿,還需要做另一件事情:實現`Cloneable`接口。這個接口使人稍覺奇怪,因為它是空的!
```
interface Cloneable {}
```
之所以要實現這個空接口,顯然不是因為我們準備向上轉換成一個`Cloneable`,以及調用它的某個方法。有些人認為在這里使用接口屬于一種“欺騙”行為,因為它使用的特性打的是別的主意,而非原來的意思。`Cloneable interface`的實現扮演了一個標記的角色,封裝到類的類型中。
兩方面的原因促成了`Cloneable interface`的存在。首先,可能有一個向上轉換引用指向一個基類型,而且不知道它是否真的能克隆那個對象。在這種情況下,可用`instanceof`關鍵字(第11章有介紹)調查引用是否確實同一個能克隆的對象連接:
```
if(myHandle instanceof Cloneable) // ...
```
第二個原因是考慮到我們可能不愿所有對象類型都能克隆。所以`Object.clone()`會驗證一個類是否真的是實現了`Cloneable`接口。若答案是否定的,則“拋”出一個`CloneNotSupportedException`異常。所以在一般情況下,我們必須將`implement Cloneable`作為對克隆能力提供支持的一部分。
## 12.2.4 成功的克隆
理解了實現`clone()`方法背后的所有細節后,便可創建出能方便復制的類,以便提供了一個本地副本:
```
//: LocalCopy.java
// Creating local copies with clone()
import java.util.*;
class MyObject implements Cloneable {
int i;
MyObject(int ii) { i = ii; }
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
System.out.println("MyObject can't clone");
}
return o;
}
public String toString() {
return Integer.toString(i);
}
}
public class LocalCopy {
static MyObject g(MyObject v) {
// Passing a handle, modifies outside object:
v.i++;
return v;
}
static MyObject f(MyObject v) {
v = (MyObject)v.clone(); // Local copy
v.i++;
return v;
}
public static void main(String[] args) {
MyObject a = new MyObject(11);
MyObject b = g(a);
// Testing handle equivalence,
// not object equivalence:
if(a == b)
System.out.println("a == b");
else
System.out.println("a != b");
System.out.println("a = " + a);
System.out.println("b = " + b);
MyObject c = new MyObject(47);
MyObject d = f(c);
if(c == d)
System.out.println("c == d");
else
System.out.println("c != d");
System.out.println("c = " + c);
System.out.println("d = " + d);
}
} ///:~
```
不管怎樣,`clone()`必須能夠訪問,所以必須將其設為`public`(公共的)。其次,作為`clone()`的初期行動,應調用`clone()`的基類版本。這里調用的`clone()`是`Object`內部預先定義好的。之所以能調用它,是由于它具有`protected`(受到保護的)屬性,所以能在派生的類里訪問。
`Object.clone()`會檢查原先的對象有多大,再為新對象騰出足夠多的內存,將所有二進制位從原來的對象復制到新對象。這叫作“按位復制”,而且按一般的想法,這個工作應該是由`clone()`方法來做的。但在`Object.clone()`正式開始操作前,首先會檢查一個類是否`Cloneable`,即是否具有克隆能力——換言之,它是否實現了`Cloneable`接口。若未實現,`Object.clone()`就拋出一個`CloneNotSupportedException`異常,指出我們不能克隆它。因此,我們最好用一個`try-catch`塊將對`super.clone()`的調用代碼包圍(或封裝)起來,試圖捕獲一個應當永不出現的異常(因為這里確實已實現了`Cloneable`接口)。
在`LocalCopy`中,兩個方法`g()`和`f()`揭示出兩種參數傳遞方法間的差異。其中,`g()`演示的是按引用傳遞,它會修改外部對象,并返回對那個外部對象的一個引用。而`f()`是對參數進行克隆,所以將其分離出來,并讓原來的對象保持獨立。隨后,它繼續做它希望的事情。甚至能返回指向這個新對象的一個引用,而且不會對原來的對象產生任何副作用。注意下面這個多少有些古怪的語句:
```
v = (MyObject)v.clone();
```
它的作用正是創建一個本地副本。為避免被這樣的一個語句搞混淆,記住這種相當奇怪的編碼形式在Java中是完全允許的,因為有一個名字的所有東西實際都是一個引用。所以引用`v`用于克隆一個它所指向的副本,而且最終返回指向基類型`Object`的一個引用(因為它在`Object.clone()`中是那樣被定義的),隨后必須將其轉換為正確的類型。
在`main()`中,兩種不同參數傳遞方式的區別在于它們分別測試了一個不同的方法。輸出結果如下:
```
a == b
a = 12
b = 12
c != d
c = 47
d = 48
```
大家要記住這樣一個事實:Java對“是否等價”的測試并不對所比較對象的內部進行檢查,從而核實它們的值是否相同。`==`和`!=`運算符只是簡單地對比引用的內容。若引用內的地址相同,就認為引用指向同樣的對象,所以認為它們是“等價”的。所以運算符真正檢測的是“由于別名問題,引用是否指向同一個對象?”
## 12.2.5 `Object.clone()`的效果
調用`Object.clone()`時,實際發生的是什么事情呢?當我們在自己的類里覆蓋`clone()`時,什么東西對于`super.clone()`來說是最關鍵的呢?根類中的`clone()`方法負責建立正確的存儲容量,并通過“按位復制”將二進制位從原始對象中復制到新對象的存儲空間。也就是說,它并不只是預留存儲空間以及復制一個對象——實際需要調查出欲復制之對象的準確大小,然后復制那個對象。由于所有這些工作都是在由根類定義之`clone()`方法的內部代碼中進行的(根類并不知道要從自己這里繼承出去什么),所以大家或許已經猜到,這個過程需要用RTTI判斷欲克隆的對象的實際大小。采取這種方式,`clone()`方法便可建立起正確數量的存儲空間,并對那個類型進行正確的按位復制。
不管我們要做什么,克隆過程的第一個部分通常都應該是調用`super.clone()`。通過進行一次準確的復制,這樣做可為后續的克隆進程建立起一個良好的基礎。隨后,可采取另一些必要的操作,以完成最終的克隆。
為確切了解其他操作是什么,首先要正確理解`Object.clone()`為我們帶來了什么。特別地,它會自動克隆所有引用指向的目標嗎?下面這個例子可完成這種形式的檢測:
```
//: Snake.java
// Tests cloning to see if destination of
// handles are also cloned.
public class Snake implements Cloneable {
private Snake next;
private char c;
// Value of i == number of segments
Snake(int i, char x) {
c = x;
if(--i > 0)
next = new Snake(i, (char)(x + 1));
}
void increment() {
c++;
if(next != null)
next.increment();
}
public String toString() {
String s = ":" + c;
if(next != null)
s += next.toString();
return s;
}
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {}
return o;
}
public static void main(String[] args) {
Snake s = new Snake(5, 'a');
System.out.println("s = " + s);
Snake s2 = (Snake)s.clone();
System.out.println("s2 = " + s2);
s.increment();
System.out.println(
"after s.increment, s2 = " + s2);
}
} ///:~
```
一條`Snake`(蛇)由數段構成,每一段的類型都是`Snake`。所以,這是一個一段段鏈接起來的列表。所有段都是以循環方式創建的,每做好一段,都會使第一個構造器參數的值遞減,直至最終為零。而為給每段賦予一個獨一無二的標記,第二個參數(一個`Char`)的值在每次循環構造器調用時都會遞增。
`increment()`方法的作用是循環遞增每個標記,使我們能看到發生的變化;而`toString`則循環打印出每個標記。輸出如下:
```
s = :a:b:c:d:e
s2 = :a:b:c:d:e
after s.increment, s2 = :a:c:d:e:f
```
這意味著只有第一段才是由`Object.clone()`復制的,所以此時進行的是一種“淺層復制”。若希望復制整條蛇——即進行“深層復制”——必須在被覆蓋的`clone()`里采取附加的操作。
通常可在從一個能克隆的類里調用`super.clone()`,以確保所有基類行動(包括`Object.clone()`)能夠進行。隨著是為對象內每個引用都明確調用一個`clone()`;否則那些引用會別名變成原始對象的引用。構造器的調用也大致相同——首先構造基類,然后是下一個派生的構造器……以此類推,直到位于最深層的派生構造器。區別在于`clone()`并不是個構造器,所以沒有辦法實現自動克隆。為了克隆,必須由自己明確進行。
## 12.2.6 克隆組合對象
試圖深層復制組合對象時會遇到一個問題。必須假定成員對象中的`clone()`方法也能依次對自己的引用進行深層復制,以此類推。這使我們的操作變得復雜。為了能正常實現深層復制,必須對所有類中的代碼進行控制,或者至少全面掌握深層復制中需要涉及的類,確保它們自己的深層復制能正確進行。
下面這個例子總結了面對一個組合對象進行深層復制時需要做哪些事情:
```
//: DeepCopy.java
// Cloning a composed object
class DepthReading implements Cloneable {
private double depth;
public DepthReading(double depth) {
this.depth = depth;
}
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
}
class TemperatureReading implements Cloneable {
private long time;
private double temperature;
public TemperatureReading(double temperature) {
time = System.currentTimeMillis();
this.temperature = temperature;
}
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
}
class OceanReading implements Cloneable {
private DepthReading depth;
private TemperatureReading temperature;
public OceanReading(double tdata, double ddata){
temperature = new TemperatureReading(tdata);
depth = new DepthReading(ddata);
}
public Object clone() {
OceanReading o = null;
try {
o = (OceanReading)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
// Must clone handles:
o.depth = (DepthReading)o.depth.clone();
o.temperature =
(TemperatureReading)o.temperature.clone();
return o; // Upcasts back to Object
}
}
public class DeepCopy {
public static void main(String[] args) {
OceanReading reading =
new OceanReading(33.9, 100.5);
// Now clone it:
OceanReading r =
(OceanReading)reading.clone();
}
} ///:~
```
`DepthReading`和`TemperatureReading`非常相似;它們都只包含了基本數據類型。所以`clone()`方法能夠非常簡單:調用`super.clone()`并返回結果即可。注意兩個類使用的`clone()`代碼是完全一致的。
`OceanReading`是由`DepthReading`和`TemperatureReading`對象合并而成的。為了對其進行深層復制,`clone()`必須同時克隆`OceanReading`內的引用。為達到這個目標,`super.clone()`的結果必須轉換成一個`OceanReading`對象(以便訪問`depth`和`temperature`引用)。
## 12.2.7 用`Vector`進行深層復制
下面讓我們復習一下本章早些時候提出的`Vector`例子。這一次`Int2`類是可以克隆的,所以能對`Vector`進行深層復制:
```
//: AddingClone.java
// You must go through a few gyrations to
// add cloning to your own class.
import java.util.*;
class Int2 implements Cloneable {
private int i;
public Int2(int ii) { i = ii; }
public void increment() { i++; }
public String toString() {
return Integer.toString(i);
}
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
System.out.println("Int2 can't clone");
}
return o;
}
}
// Once it's cloneable, inheritance
// doesn't remove cloneability:
class Int3 extends Int2 {
private int j; // Automatically duplicated
public Int3(int i) { super(i); }
}
public class AddingClone {
public static void main(String[] args) {
Int2 x = new Int2(10);
Int2 x2 = (Int2)x.clone();
x2.increment();
System.out.println(
"x = " + x + ", x2 = " + x2);
// Anything inherited is also cloneable:
Int3 x3 = new Int3(7);
x3 = (Int3)x3.clone();
Vector v = new Vector();
for(int i = 0; i < 10; i++ )
v.addElement(new Int2(i));
System.out.println("v: " + v);
Vector v2 = (Vector)v.clone();
// Now clone each element:
for(int i = 0; i < v.size(); i++)
v2.setElementAt(
((Int2)v2.elementAt(i)).clone(), i);
// Increment all v2's elements:
for(Enumeration e = v2.elements();
e.hasMoreElements(); )
((Int2)e.nextElement()).increment();
// See if it changed v's elements:
System.out.println("v: " + v);
System.out.println("v2: " + v2);
}
} ///:~
```
`Int3`自`Int2`繼承而來,并添加了一個新的基本類型成員`int j`。大家也許認為自己需要再次覆蓋`clone()`,以確保`j`得到復制,但實情并非如此。將`Int2`的`clone()`當作`Int3`的`clone()`調用時,它會調用`Object.clone()`,判斷出當前操作的是`Int3`,并復制`Int3`內的所有二進制位。只要沒有新增需要克隆的引用,對`Object.clone()`的一個調用就能完成所有必要的復制——無論`clone()`是在層次結構多深的一級定義的。
至此,大家可以總結出對`Vector`進行深層復制的先決條件:在克隆了`Vector`后,必須在其中遍歷,并克隆由`Vector`指向的每個對象。為了對`Hashtable`(散列表)進行深層復制,也必須采取類似的處理。
這個例子剩余的部分顯示出克隆已實際進行——證據就是在克隆了對象以后,可以自由改變它,而原來那個對象不受任何影響。
## 12.2.8 通過序列化進行深層復制
若研究一下第10章介紹的那個Java 1.1對象序列化示例,可能發現若在一個對象序列化以后再撤消對它的序列化,或者說進行裝配,那么實際經歷的正是一個“克隆”的過程。
那么為什么不用序列化進行深層復制呢?下面這個例子通過計算執行時間對比了這兩種方法:
```
//: Compete.java
import java.io.*;
class Thing1 implements Serializable {}
class Thing2 implements Serializable {
Thing1 o1 = new Thing1();
}
class Thing3 implements Cloneable {
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
System.out.println("Thing3 can't clone");
}
return o;
}
}
class Thing4 implements Cloneable {
Thing3 o3 = new Thing3();
public Object clone() {
Thing4 o = null;
try {
o = (Thing4)super.clone();
} catch (CloneNotSupportedException e) {
System.out.println("Thing4 can't clone");
}
// Clone the field, too:
o.o3 = (Thing3)o3.clone();
return o;
}
}
public class Compete {
static final int SIZE = 5000;
public static void main(String[] args) {
Thing2[] a = new Thing2[SIZE];
for(int i = 0; i < a.length; i++)
a[i] = new Thing2();
Thing4[] b = new Thing4[SIZE];
for(int i = 0; i < b.length; i++)
b[i] = new Thing4();
try {
long t1 = System.currentTimeMillis();
ByteArrayOutputStream buf =
new ByteArrayOutputStream();
ObjectOutputStream o =
new ObjectOutputStream(buf);
for(int i = 0; i < a.length; i++)
o.writeObject(a[i]);
// Now get copies:
ObjectInputStream in =
new ObjectInputStream(
new ByteArrayInputStream(
buf.toByteArray()));
Thing2[] c = new Thing2[SIZE];
for(int i = 0; i < c.length; i++)
c[i] = (Thing2)in.readObject();
long t2 = System.currentTimeMillis();
System.out.println(
"Duplication via serialization: " +
(t2 - t1) + " Milliseconds");
// Now try cloning:
t1 = System.currentTimeMillis();
Thing4[] d = new Thing4[SIZE];
for(int i = 0; i < d.length; i++)
d[i] = (Thing4)b[i].clone();
t2 = System.currentTimeMillis();
System.out.println(
"Duplication via cloning: " +
(t2 - t1) + " Milliseconds");
} catch(Exception e) {
e.printStackTrace();
}
}
} ///:~
```
其中,`Thing2`和`Thing4`包含了成員對象,所以需要進行一些深層復制。一個有趣的地方是盡管`Serializable`類很容易設置,但在復制它們時卻要做多得多的工作。克隆涉及到大量的類設置工作,但實際的對象復制是相當簡單的。結果很好地說明了一切。下面是幾次運行分別得到的結果:
```
Duplication via serialization: 3400 Milliseconds
Duplication via cloning: 110 Milliseconds
Duplication via serialization: 3410 Milliseconds
Duplication via cloning: 110 Milliseconds
Duplication via serialization: 3520 Milliseconds
Duplication via cloning: 110 Milliseconds
```
除了序列化和克隆之間巨大的時間差異以外,我們也注意到序列化技術的運行結果并不穩定,而克隆每一次花費的時間都是相同的。
## 12.2.9 使克隆具有更大的深度
若新建一個類,它的基類會默認為`Object`,并默認為不具備克隆能力(就象在下一節會看到的那樣)。只要不明確地添加克隆能力,這種能力便不會自動產生。但我們可以在任何層添加它,然后便可從那個層開始向下具有克隆能力。如下所示:
```
//: HorrorFlick.java
// You can insert Cloneability at any
// level of inheritance.
import java.util.*;
class Person {}
class Hero extends Person {}
class Scientist extends Person
implements Cloneable {
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
// this should never happen:
// It's Cloneable already!
throw new InternalError();
}
}
}
class MadScientist extends Scientist {}
public class HorrorFlick {
public static void main(String[] args) {
Person p = new Person();
Hero h = new Hero();
Scientist s = new Scientist();
MadScientist m = new MadScientist();
// p = (Person)p.clone(); // Compile error
// h = (Hero)h.clone(); // Compile error
s = (Scientist)s.clone();
m = (MadScientist)m.clone();
}
} ///:~
```
添加克隆能力之前,編譯器會阻止我們的克隆嘗試。一旦在`Scientist`里添加了克隆能力,那么`Scientist`以及它的所有“后裔”都可以克隆。
## 12.2.10 為什么有這個奇怪的設計
之所以感覺這個方案的奇特,因為它事實上的確如此。也許大家會奇怪它為什么要象這樣運行,而該方案背后的真正含義是什么呢?后面講述的是一個未獲證實的故事——大概是由于圍繞Java的許多買賣使其成為一種設計優良的語言——但確實要花許多口舌才能講清楚這背后發生的所有事情。
最初,Java只是作為一種用于控制硬件的語言而設計,與因特網并沒有絲毫聯系。象這樣一類面向大眾的語言一樣,其意義在于程序員可以對任意一個對象進行克隆。這樣一來,`clone()`就放置在根類`Object`里面,但因為它是一種公用方式,因而我們通常能夠對任意一個對象進行克隆。看來這是最靈活的方式了,畢竟它不會帶來任何害處。
正當Java看起來象一種終級因特網程序設計語言的時候,情況卻發生了變化。突然地,人們提出了安全問題,而且理所當然,這些問題與使用對象有關,我們不愿望任何人克隆自己的保密對象。所以我們最后看到的是為原來那個簡單、直觀的方案添加的大量補丁:`clone()`在`Object`里被設置成`protected`。必須將其覆蓋,并使用`implement Cloneable`,同時解決異常的問題。
只有在準備調用`Object`的`clone()`方法時,才沒有必要使用`Cloneable`接口,因為那個方法會在運行期間得到檢查,以確保我們的類實現了`Cloneable`。但為了保持連貫性(而且由于`Cloneable`無論如何都是空的),最好還是由自己實現`Cloneable`。
- 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 推薦讀物