java編程思想(第四版)_第12章 傳遞和返回對象

-------------------------------------------------

本教程由yyc,spirit整理

-------------------------------------------------



到目前爲止,讀者應對對象的“傳遞”有了一個較爲深刻的認識,記住實際傳遞的只是一個句柄。
在許多程序設計語言中,我們可用語言的“普通”方式到處傳遞對象,而且大多數時候都不會遇到問題。但有些時候卻不得不採取一些非常做法,使得情況突然變得稍微複雜起來(在C++中則是變得非常複雜)。Java亦不例外,我們十分有必要準確認識在對象傳遞和賦值時所發生的一切。這正是本章的宗旨。
若讀者是從某些特殊的程序設計環境中轉移過來的,那麼一般都會問到:“Java有指針嗎?”有些人認爲指針的操作很困難,而且十分危險,所以一廂情願地認爲它沒有好處。同時由於Java有如此好的口碑,所以應該很輕易地免除自己以前編程中的麻煩,其中不可能夾帶有指針這樣的“危險品”。然而準確地說,Java是有指針的!事實上,Java中每個對象(除基本數據類型以外)的標識符都屬於指針的一種。但它們的使用受到了嚴格的限制和防範,不僅編譯器對它們有“戒心”,運行期系統也不例外。或者換從另一個角度說,Java有指針,但沒有傳統指針的麻煩。我曾一度將這種指針叫做“句柄”,但你可以把它想像成“安全指針”。和預備學校爲學生提供的安全剪刀類似——除非特別有意,否則不會傷着自己,只不過有時要慢慢來,要習慣一些沉悶的工作。

12.1 傳遞句柄
將句柄傳遞進入一個方法時,指向的仍然是相同的對象。一個簡單的實驗可以證明這一點(若執行這個程序時有麻煩,請參考第3章3.1.2小節“賦值”):


 c12;

  PassHandles {
    f(PassHandles h) {
    System.out.println( + h);
  }
     main(String[] args) {
    PassHandles p =  PassHandles();
    System.out.println( + p);
    f(p);
  }
} 

toString方法會在打印語句裏自動調用,而PassHandles直接從Object繼承,沒有toString的重新定義。因此,這裏會採用toString的Object版本,打印出對象的類,接着是那個對象所在的位置(不是句柄,而是對象的實際存儲位置)。輸出結果如下:
p inside main(): PassHandles@1653748
h inside f() : PassHandles@1653748
可以看到,無論p還是h引用的都是同一個對象。這比複製一個新的PassHandles對象有效多了,使我們能將一個參數發給一個方法。但這樣做也帶來了另一個重要的問題。

12.1.1 別名問題
“別名”意味着多個句柄都試圖指向同一個對象,就象前面的例子展示的那樣。若有人向那個對象裏寫入一點什麼東西,就會產生別名問題。若其他句柄的所有者不希望那個對象改變,恐怕就要失望了。這可用下面這個簡單的例子說明:


  Alias1 {
   i;
  Alias1( ii) { i = ii; }
     main(String[] args) {
    Alias1 x =  Alias1(7);
    Alias1 y = x; 
    System.out.println( + x.i);
    System.out.println( + y.i);
    System.out.println();
    x.i++;
    System.out.println( + x.i);
    System.out.println( + y.i);
  }
} 

對下面這行:
Alias1 y = x; // Assign the handle
它會新建一個Alias1句柄,但不是把它分配給由new創建的一個新鮮對象,而是分配給一個現有的句柄。所以句柄x的內容——即對象x指向的地址——被分配給y,所以無論x還是y都與相同的對象連接起來。這樣一來,一旦x的i在下述語句中增值:
x.i++;
y的i值也必然受到影響。從最終的輸出就可以看出:
x: 7
y: 7
Incrementing x
x: 8
y: 8

此時最直接的一個解決辦法就是乾脆不這樣做:不要有意將多個句柄指向同一個作用域內的同一個對象。這樣做可使代碼更易理解和調試。然而,一旦準備將句柄作爲一個自變量或參數傳遞——這是Java設想的正常方法——別名問題就會自動出現,因爲創建的本地句柄可能修改“外部對象”(在方法作用域之外創建的對象)。下面是一個例子:



  Alias2 {
   i;
  Alias2( ii) { i = ii; }
    f(Alias2 handle) {
    handle.i++;
  }
     main(String[] args) {
    Alias2 x =  Alias2(7);
    System.out.println( + x.i);
    System.out.println();
    f(x);
    System.out.println( + x.i);
  }
} 

輸出如下:
x: 7
Calling f(x)
x: 8

方法改變了自己的參數——外部對象。一旦遇到這種情況,必須判斷它是否合理,用戶是否願意這樣,以及是不是會造成問題。
通常,我們調用一個方法是爲了產生返回值,或者用它改變爲其調用方法的那個對象的狀態(方法其實就是我們向那個對象“發一條消息”的方式)。很少需要調用一個方法來處理它的參數;這叫作利用方法的“副作用”(Side Effect)。所以倘若創建一個會修改自己參數的方法,必須向用戶明確地指出這一情況,並警告使用那個方法可能會有的後果以及它的潛在威脅。由於存在這些混淆和缺陷,所以應該儘量避免改變參數。
若需在一個方法調用期間修改一個參數,且不打算修改外部參數,就應在自己的方法內部製作一個副本,從而保護那個參數。本章的大多數內容都是圍繞這個問題展開的。

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(),如下所示:


 java.util.*;

 Int {
    i;
   Int( ii) { i = ii; }
    increment() { i++; }
   String toString() { 
     Integer.toString(i); 
  }
}

  Cloning {
     main(String[] args) {
    Vector v =  Vector();
    ( i = 0; i < 10; i++ )
      v.addElement( Int(i));
    System.out.println( + v);
    Vector v2 = (Vector)v.clone();
    
    (Enumeration e = v2.elements();
        e.hasMoreElements(); )
      ((Int)e.nextElement()).increment();
    
    System.out.println( + 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()方法背後的所有細節後,便可創建出能方便複製的類,以便提供了一個本地副本:

 java.util.*;

 MyObject  Cloneable {
   i;
  MyObject( ii) { i = ii; }
   Object clone() {
    Object o = ;
     {
      o = .clone();
    }  (CloneNotSupportedException e) {
      System.out.println();
    }
     o;
  }
   String toString() {
     Integer.toString(i);
  }
}

  LocalCopy {
   MyObject g(MyObject v) {
    
    v.i++;
     v;
  }
   MyObject f(MyObject v) {
    v = (MyObject)v.clone(); 
    v.i++;
     v;
  }
     main(String[] args) {
    MyObject a =  MyObject(11);
    MyObject b = g(a);
    
    
    (a == b) 
      System.out.println();
     
      System.out.println();
    System.out.println( + a);
    System.out.println( + b);
    MyObject c =  MyObject(47);
    MyObject d = f(c);
    (c == d) 
      System.out.println();
     
      System.out.println();
    System.out.println( + c);
    System.out.println( + 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  Cloneable {
   Snake next;
    c;
  
  Snake( i,  x) {
    c = x;
    (--i > 0)
      next =  Snake(i, ()(x + 1));
  }
   increment() {
    c++;
    (next != )
      next.increment();
  }
   String toString() {
    String s =  + c;
    (next != )
      s += next.toString();
     s;
  }
   Object clone() {
    Object o = ;
     {
      o = .clone();
    }  (CloneNotSupportedException e) {}
     o;
  }
     main(String[] args) {
    Snake s =  Snake(5, 'a');
    System.out.println( + s);
    Snake s2 = (Snake)s.clone();
    System.out.println( + s2);
    s.increment();
    System.out.println(
       + 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()方法也能依次對自己的句柄進行深層複製,以此類推。這使我們的操作變得複雜。爲了能正常實現深層複製,必須對所有類中的代碼進行控制,或者至少全面掌握深層複製中需要涉及的類,確保它們自己的深層複製能正確進行。
下面這個例子總結了面對一個合成對象進行深層複製時需要做哪些事情:


 DepthReading  Cloneable {
    depth;
   DepthReading( depth) { 
    .depth = depth;
  }
   Object clone() {
    Object o = ;
     {
      o = .clone();
    }  (CloneNotSupportedException e) {
      e.printStackTrace();
    }
     o;
  }
}

 TemperatureReading  Cloneable {
    time;
    temperature;
   TemperatureReading( temperature) {
    time = System.currentTimeMillis();
    .temperature = temperature;
  }
   Object clone() {
    Object o = ;
     {
      o = .clone();
    }  (CloneNotSupportedException e) {
      e.printStackTrace();
    }
     o;
  }
}

 OceanReading  Cloneable {
   DepthReading depth;
   TemperatureReading temperature;
   OceanReading( tdata,  ddata){
    temperature =  TemperatureReading(tdata);
    depth =  DepthReading(ddata);
  }
   Object clone() {
    OceanReading o = ;
     {
      o = (OceanReading).clone();
    }  (CloneNotSupportedException e) {
      e.printStackTrace();
    }
    
    o.depth = (DepthReading)o.depth.clone();
    o.temperature = 
      (TemperatureReading)o.temperature.clone();
     o; 
  }
}

  DeepCopy {
     main(String[] args) {
    OceanReading reading = 
       OceanReading(33.9, 100.5);
    
    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進行深層複製:


 java.util.*;

 Int2  Cloneable {
    i;
   Int2( ii) { i = ii; }
    increment() { i++; }
   String toString() {
     Integer.toString(i);
  }
   Object clone() {
    Object o = ;
     {
      o = .clone();
    }  (CloneNotSupportedException e) {
      System.out.println();
    }
     o;
  }
}



 Int3  Int2 {
    j; 
   Int3( i) { (i); }
}

  AddingClone {
     main(String[] args) {
    Int2 x =  Int2(10);
    Int2 x2 = (Int2)x.clone();
    x2.increment();
    System.out.println(
       + x +  + x2);
    
    Int3 x3 =  Int3(7);
    x3 = (Int3)x3.clone();

    Vector v =  Vector();
    ( i = 0; i < 10; i++ )
      v.addElement( Int2(i));
    System.out.println( + v);
    Vector v2 = (Vector)v.clone();
    
    ( i = 0; i < v.size(); i++)
      v2.setElementAt(
        ((Int2)v2.elementAt(i)).clone(), i);
    
    (Enumeration e = v2.elements();
        e.hasMoreElements(); )
      ((Int2)e.nextElement()).increment();
    
    System.out.println( + v);
    System.out.println( + 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對象序列化示例,可能發現若在一個對象序列化以後再撤消對它的序列化,或者說進行裝配,那麼實際經歷的正是一個“克隆”的過程。
那麼爲什麼不用序列化進行深層複製呢?下面這個例子通過計算執行時間對比了這兩種方法:
 java.io.*;

 Thing1  Serializable {}
 Thing2  Serializable {
  Thing1 o1 =  Thing1();
}

 Thing3  Cloneable {
   Object clone() {
    Object o = ;
     {
      o = .clone();
    }  (CloneNotSupportedException e) {
      System.out.println();
    }
     o;
  }
}

 Thing4  Cloneable {
  Thing3 o3 =  Thing3();
   Object clone() {
    Thing4 o = ;
     {
      o = (Thing4).clone();
    }  (CloneNotSupportedException e) {
      System.out.println();
    }
    
    o.o3 = (Thing3)o3.clone();
     o;
  }
}

  Compete {
     SIZE = 5000;
     main(String[] args) {
    Thing2[] a =  Thing2[SIZE];
    ( i = 0; i < a.length; i++)
      a[i] =  Thing2();
    Thing4[] b =  Thing4[SIZE];
    ( i = 0; i < b.length; i++)
      b[i] =  Thing4();
     {
       t1 = System.currentTimeMillis();
      ByteArrayOutputStream buf = 
         ByteArrayOutputStream();
      ObjectOutputStream o =
         ObjectOutputStream(buf);
      ( i = 0; i < a.length; i++)
        o.writeObject(a[i]);
      
      ObjectInputStream in =
         ObjectInputStream(
           ByteArrayInputStream(
            buf.toByteArray()));
      Thing2[] c =  Thing2[SIZE];
      ( i = 0; i < c.length; i++)
        c[i] = (Thing2)in.readObject();
       t2 = System.currentTimeMillis();
      System.out.println(
         +
        (t2 - t1) + );
      
      t1 = System.currentTimeMillis();
      Thing4[] d =  Thing4[SIZE];
      ( i = 0; i < d.length; i++)
        d[i] = (Thing4)b[i].clone();
      t2 = System.currentTimeMillis();
      System.out.println(
         +
        (t2 - t1) + );
    } (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,並默認爲不具備克隆能力(就象在下一節會看到的那樣)。只要不明確地添加克隆能力,這種能力便不會自動產生。但我們可以在任何層添加它,然後便可從那個層開始向下具有克隆能力。如下所示:


 java.util.*;

 Person {}
 Hero  Person {}
 Scientist  Person 
     Cloneable {
   Object clone() {
     {
       .clone();
    }  (CloneNotSupportedException e) {
      
      
        InternalError();
    }
  }
}
 MadScientist  Scientist {}

  HorrorFlick {
     main(String[] args) {
    Person p =  Person();
    Hero h =  Hero();
    Scientist s =  Scientist();
    MadScientist m =  MadScientist();

    
    
    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。

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(獨子)方案。

下面這個例子總結了克隆的各種實現方法,然後在層次結構中將其“關閉”:




 Ordinary {}



 WrongClone  Ordinary {
   Object clone()
       CloneNotSupportedException {
     .clone(); 
  }
}


 IsCloneable  Ordinary 
     Cloneable {
   Object clone() 
       CloneNotSupportedException {
     .clone();
  }
}


 NoMore  IsCloneable {
   Object clone() 
       CloneNotSupportedException {
      CloneNotSupportedException();
  }
}

 TryMore  NoMore {
   Object clone() 
       CloneNotSupportedException {
    
     .clone();
  }
}

 BackOn  NoMore {
   BackOn duplicate(BackOn b) {
    
    
    
      BackOn();
  }
   Object clone() {
    
     duplicate();
  }
}



  ReallyNoMore  NoMore {}

  CheckCloneable {
   Ordinary tryToClone(Ordinary ord) {
    String id = ord.getClass().getName();
    Ordinary x = ;
    (ord  Cloneable) {
       {
        System.out.println( + id);
        x = (Ordinary)((IsCloneable)ord).clone();
        System.out.println( + id);
      } (CloneNotSupportedException e) {
        System.out.println(
           + id);
      }
    }
     x;
  }
     main(String[] args) {
    
    Ordinary[] ord = { 
       IsCloneable(),
       WrongClone(),
       NoMore(),
       TryMore(),
       BackOn(),
       ReallyNoMore(),
    };
    Ordinary x =  Ordinary();
    
    
    
    
    
    ( 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++程序員,這個方法就更顯親切)。下面是一個實際的例子:




 FruitQualities {
    weight;
    color;
    firmness;
    ripeness;
    smell;
  
  FruitQualities() { 
    
  }
  
  
  
  FruitQualities(FruitQualities f) {
    weight = f.weight;
    color = f.color;
    firmness = f.firmness;
    ripeness = f.ripeness;
    smell = f.smell;
    
  }
}

 Seed {
  
  Seed() {  }
  Seed(Seed s) {  }
}

 Fruit {
   FruitQualities fq;
    seeds;
   Seed[] s;
  Fruit(FruitQualities q,  seedCount) { 
    fq = q;
    seeds = seedCount;
    s =  Seed[seeds];
    ( i = 0; i < seeds; i++)
      s[i] =  Seed();
  }
  
  
  
  Fruit(Fruit f) {
    fq =  FruitQualities(f.fq);
    seeds = f.seeds;
    
    ( i = 0; i < seeds; i++)
      s[i] =  Seed(f.s[i]);
    
  }
  
  
    addQualities(FruitQualities q) {
    fq = q;
  }
   FruitQualities getQualities() {
     fq;
  }
}

 Tomato  Fruit {
  Tomato() {
    ( FruitQualities(), 100);
  }
  Tomato(Tomato t) { 
    (t); 
    
  }
}

 ZebraQualities  FruitQualities {
    stripedness;
  ZebraQualities() { 
    
  }
  ZebraQualities(ZebraQualities z) {
    (z);
    stripedness = z.stripedness;
  }
}

 GreenZebra  Tomato {
  GreenZebra() {
    addQualities( ZebraQualities());
  }
  GreenZebra(GreenZebra g) {
    (g); 
    
    addQualities( ZebraQualities());
  }
   evaluate() {
    ZebraQualities zq = 
      (ZebraQualities)getQualities();
    
    
  }
}

  CopyConstructor {
     ripen(Tomato t) {
    
    t =  Tomato(t); 
    System.out.println( +
      t.getClass().getName());
  }
     slice(Fruit f) {
    f =  Fruit(f); 
    System.out.println( +
      f.getClass().getName());
  }
     main(String[] args) {
    Tomato tomato =  Tomato();
    ripen(tomato); 
    slice(tomato); 
    GreenZebra g =  GreenZebra();
    ripen(g); 
    slice(g); 
    g.evaluate();
  }
} 

這個例子第一眼看上去顯得有點奇怪。不同水果的質量肯定有所區別,但爲什麼只是把代表那些質量的數據成員直接置入Fruit(水果)類?有兩方面可能的原因。第一個是我們可能想簡便地插入或修改質量。注意Fruit有一個protected(受到保護的)addQualities()方法,它允許衍生類來進行這些插入或修改操作(大家或許會認爲最合乎邏輯的做法是在Fruit中使用一個protected構建器,用它獲取FruitQualities參數,但構建器不能繼承,所以不可在第二級或級數更深的類中使用它)。通過將水果的質量置入一個獨立的類,可以得到更大的靈活性,其中包括可以在特定Fruit對象的存在期間中途更改質量。
之所以將FruitQualities設爲一個獨立的對象,另一個原因是考慮到我們有時希望添加新的質量,或者通過繼承與多形性改變行爲。注意對GreenZebra來說(這實際是西紅柿的一類——我已栽種成功,它們簡直令人難以置信),構建器會調用addQualities(),併爲其傳遞一個ZebraQualities對象。該對象是從FruitQualities衍生出來的,所以能與基礎類中的FruitQualities句柄聯繫在一起。當然,一旦GreenZebra使用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裏是很不通的,所以不要用它。

12.4 只讀類
儘管在一些特定的場合,由clone()產生的本地副本能夠獲得我們希望的結果,但程序員(方法的作者)不得不親自禁止別名處理的副作用。假如想製作一個庫,令其具有常規用途,但卻不能擔保它肯定能在正確的類中得以克隆,這時又該怎麼辦呢?更有可能的一種情況是,假如我們想讓別名發揮積極的作用——禁止不必要的對象複製——但卻不希望看到由此造成的副作用,那麼又該如何處理呢?
一個辦法是創建“不變對象”,令其從屬於只讀類。可定義一個特殊的類,使其中沒有任何方法能造成對象內部狀態的改變。在這樣的一個類中,別名處理是沒有問題的。因爲我們只能讀取內部狀態,所以當多處代碼都讀取相同的對象時,不會出現任何副作用。
作爲“不變對象”一個簡單例子,Java的標準庫包含了“封裝器”(wrapper)類,可用於所有基本數據類型。大家可能已發現了這一點,如果想在一個象Vector(只採用Object句柄)這樣的集合裏保存一個int數值,可以將這個int封裝到標準庫的Integer類內部。如下所示:

 java.util.*;

  ImmutableInteger {
     main(String[] args) {
    Vector v =  Vector();
    ( i = 0; i < 10; i++) 
      v.addElement( Integer(i));
    
    
  }
} 

Integer類(以及基本的“封裝器”類)用簡單的形式實現了“不變性”:它們沒有提供可以修改對象的方法。
若確實需要一個容納了基本數據類型的對象,並想對基本數據類型進行修改,就必須親自創建它們。幸運的是,操作非常簡單:

 java.util.*;

 IntValue { 
   n;
  IntValue( x) { n = x; }
   String toString() { 
     Integer.toString(n);
  }
}

  MutableInteger {
     main(String[] args) {
    Vector v =  Vector();
    ( i = 0; i < 10; i++) 
      v.addElement( IntValue(i));
    System.out.println(v);
    ( i = 0; i < v.size(); i++)
      ((IntValue)v.elementAt(i)).n++;
    System.out.println(v);
  }
} 

注意n在這裏簡化了我們的編碼。
若默認的初始化爲零已經足夠(便不需要構建器),而且不用考慮把它打印出來(便不需要toString),那麼IntValue甚至還能更加簡單。如下所示:
class IntValue { int n; }
將元素取出來,再對其進行造型,這多少顯得有些笨拙,但那是Vector的問題,不是IntValue的錯。

12.4.1 創建只讀類
完全可以創建自己的只讀類,下面是個簡單的例子:



  Immutable1 {
    data;
   Immutable1( initVal) {
    data = initVal;
  }
    read() {  data; }
    nonzero() {  data != 0; }
   Immutable1 quadruple() {
      Immutable1(data * 4);
  }
    f(Immutable1 i1) {
    Immutable1 quad = i1.quadruple();
    System.out.println( + i1.read());
    System.out.println( + quad.read());
  }
     main(String[] args) {
    Immutable1 x =  Immutable1(47);
    System.out.println( + x.read());
    f(x);
    System.out.println( + x.read());
  }
} 

所有數據都設爲private,可以看到沒有任何public方法對數據作出修改。事實上,確實需要修改一個對象的方法是quadruple(),但它的作用是新建一個Immutable1對象,初始對象則是原封未動的。
方法f()需要取得一個Immutable1對象,並對其採取不同的操作,而main()的輸出顯示出沒有對x作任何修改。因此,x對象可別名處理許多次,不會造成任何傷害,因爲根據Immutable1類的設計,它能保證對象不被改動。

12.4.2 “一成不變”的弊端
從表面看,不變類的建立似乎是一個好方案。但是,一旦真的需要那種新類型的一個修改的對象,就必須辛苦地進行新對象的創建工作,同時還有可能涉及更頻繁的垃圾收集。對有些類來說,這個問題並不是很大。但對其他類來說(比如String類),這一方案的代價顯得太高了。
爲解決這個問題,我們可以創建一個“同志”類,並使其能夠修改。以後只要涉及大量的修改工作,就可換爲使用能修改的同志類。完事以後,再切換回不可變的類。
因此,上例可改成下面這個樣子:



 Mutable {
    data;
   Mutable( initVal) {
    data = initVal;
  }
   Mutable add( x) { 
    data += x;
     ;
  }
   Mutable multiply( x) {
    data *= x;
     ;
  }
   Immutable2 makeImmutable2() {
      Immutable2(data);
  }
}

  Immutable2 {
    data;
   Immutable2( initVal) {
    data = initVal;
  }
    read() {  data; }
    nonzero() {  data != 0; }
   Immutable2 add( x) { 
      Immutable2(data + x);
  }
   Immutable2 multiply( x) {
      Immutable2(data * x);
  }
   Mutable makeMutable() {
      Mutable(data);
  }
    Immutable2 modify1(Immutable2 y){
    Immutable2 val = y.add(12);
    val = val.multiply(3);
    val = val.add(11);
    val = val.multiply(2);
     val;
  }
  
    Immutable2 modify2(Immutable2 y){
    Mutable m = y.makeMutable();
    m.add(12).multiply(3).add(11).multiply(2);
     m.makeImmutable2();
  }
     main(String[] args) {
    Immutable2 i2 =  Immutable2(47);
    Immutable2 r1 = modify1(i2);
    Immutable2 r2 = modify2(i2);
    System.out.println( + i2.read());
    System.out.println( + r1.read());
    System.out.println( + r2.read());
  }
} 

和往常一樣,Immutable2包含的方法保留了對象不可變的特徵,只要涉及修改,就創建新的對象。完成這些操作的是add()和multiply()方法。同志類叫作Mutable,它也含有add()和multiply()方法。但這些方法能夠修改Mutable對象,而不是新建一個。除此以外,Mutable的一個方法可用它的數據產生一個Immutable2對象,反之亦然。
兩個靜態方法modify1()和modify2()揭示出獲得同樣結果的兩種不同方法。在modify1()中,所有工作都是在Immutable2類中完成的,我們可看到在進程中創建了四個新的Immutable2對象(而且每次重新分配了val,前一個對象就成爲垃圾)。
在方法modify2()中,可看到它的第一個行動是獲取Immutable2 y,然後從中生成一個Mutable(類似於前面對clone()的調用,但這一次創建了一個不同類型的對象)。隨後,用Mutable對象進行大量修改操作,同時用不着新建許多對象。最後,它切換回Immutable2。在這裏,我們只創建了兩個新對象(Mutable和Immutable2的結果),而不是四個。
這一方法特別適合在下述場合應用:
(1) 需要不可變的對象,而且
(2) 經常需要進行大量修改,或者
(3) 創建新的不變對象代價太高

12.4.3 不變字串
請觀察下述代碼:

  Stringer {
   String upcase(String s) {
     s.toUpperCase();
  }
     main(String[] args) {
    String q =  String();
    System.out.println(q); 
    String qq = upcase(q);
    System.out.println(qq); 
    System.out.println(q); 
  }
} 

q傳遞進入upcase()時,它實際是q的句柄的一個副本。該句柄連接的對象實際只在一個統一的物理位置處。句柄四處傳遞的時候,它的句柄會得到複製。
若觀察對upcase()的定義,會發現傳遞進入的句柄有一個名字s,而且該名字只有在upcase()執行期間纔會存在。upcase()完成後,本地句柄s便會消失,而upcase()返回結果——還是原來那個字串,只是所有字符都變成了大寫。當然,它返回的實際是結果的一個句柄。但它返回的句柄最終是爲一個新對象的,同時原來的q並未發生變化。所有這些是如何發生的呢?

1. 隱式常數
若使用下述語句:
String s = "asdf";
String x = Stringer.upcase(s);
那麼真的希望upcase()方法改變自變量或者參數嗎?我們通常是不願意的,因爲作爲提供給方法的一種信息,自變量一般是拿給代碼的讀者看的,而不是讓他們修改。這是一個相當重要的保證,因爲它使代碼更易編寫和理解。
爲了在C++中實現這一保證,需要一個特殊關鍵字的幫助:const。利用這個關鍵字,程序員可以保證一個句柄(C++叫“指針”或者“引用”)不會被用來修改原始的對象。但這樣一來,C++程序員需要用心記住在所有地方都使用const。這顯然易使人混淆,也不容易記住。

2. 覆蓋"+"和StringBuffer
利用前面提到的技術,String類的對象被設計成“不可變”。若查閱聯機文檔中關於String類的內容(本章稍後還要總結它),就會發現類中能夠修改String的每個方法實際都創建和返回了一個嶄新的String對象,新對象裏包含了修改過的信息——原來的String是原封未動的。因此,Java裏沒有與C++的const對應的特性可用來讓編譯器支持對象的不可變能力。若想獲得這一能力,可以自行設置,就象String那樣。
由於String對象是不可變的,所以能夠根據情況對一個特定的String進行多次別名處理。因爲它是隻讀的,所以一個句柄不可能會改變一些會影響其他句柄的東西。因此,只讀對象可以很好地解決別名問題。
通過修改產生對象的一個嶄新版本,似乎可以解決修改對象時的所有問題,就象String那樣。但對某些操作來講,這種方法的效率並不高。一個典型的例子便是爲String對象覆蓋的運算符“+”。“覆蓋”意味着在與一個特定的類使用時,它的含義已發生了變化(用於String的“+”和“+=”是Java中能被覆蓋的唯一運算符,Java不允許程序員覆蓋其他任何運算符——註釋④)。

④:C++允許程序員隨意覆蓋運算符。由於這通常是一個複雜的過程(參見《Thinking in C++》,Prentice-Hall於1995年出版),所以Java的設計者認定它是一種“糟糕”的特性,決定不在Java中採用。但具有諷剌意味的是,運算符的覆蓋在Java中要比在C++中容易得多。

針對String對象使用時,“+”允許我們將不同的字串連接起來:
String s =  + foo +  + Integer.toString(47);

可以想象出它“可能”是如何工作的:字串"abc"可以有一個方法append(),它新建了一個字串,其中包含"abc"以及foo的內容;這個新字串然後再創建另一個新字串,在其中添加"def";以此類推。
這一設想是行得通的,但它要求創建大量字串對象。儘管最終的目的只是獲得包含了所有內容的一個新字串,但中間卻要用到大量字串對象,而且要不斷地進行垃圾收集。我懷疑Java的設計者是否先試過種方法(這是軟件開發的一個教訓——除非自己試試代碼,並讓某些東西運行起來,否則不可能真正瞭解系統)。我還懷疑他們是否早就發現這樣做獲得的性能是不能接受的。
解決的方法是象前面介紹的那樣製作一個可變的同志類。對字串來說,這個同志類叫作StringBuffer,編譯器可以自動創建一個StringBuffer,以便計算特定的表達式,特別是面向String對象應用覆蓋過的運算符+和+=時。下面這個例子可以解決這個問題:


  ImmutableStrings {
     main(String[] args) {
    String foo = ;
    String s =  + foo +
       + Integer.toString(47);
    System.out.println(s);
    
    StringBuffer sb = 
       StringBuffer(); 
    sb.append(foo);
    sb.append(); 
    sb.append(Integer.toString(47));
    System.out.println(sb);
  }
} 

創建字串s時,編譯器做的工作大致等價於後面使用sb的代碼——創建一個StringBuffer,並用append()將新字符直接加入StringBuffer對象(而不是每次都產生新對象)。儘管這樣做更有效,但不值得每次都創建象"abc"和"def"這樣的引號字串,編譯器會把它們都轉換成String對象。所以儘管StringBuffer提供了更高的效率,但會產生比我們希望的多得多的對象。

12.4.4 String和StringBuffer類
這裏總結一下同時適用於String和StringBuffer的方法,以便對它們相互間的溝通方式有一個印象。這些表格並未把每個單獨的方法都包括進去,而是包含了與本次討論有重要關係的方法。那些已被覆蓋的方法用單獨一行總結。
首先總結String類的各種方法:

方法 自變量,覆蓋 用途

構建器 已被覆蓋:默認,String,StringBuffer,char數組,byte數組 創建String對象
length() 無 String中的字符數量
charAt() int Index 位於String內某個位置的char
getChars(),getBytes 開始複製的起點和終點,要向其中複製內容的數組,對目標數組的一個索引 將char或byte複製到外部數組內部
toCharArray() 無 產生一個char[],其中包含了String內部的字符
equals(),equalsIgnoreCase() 用於對比的一個String 對兩個字串的內容進行等價性檢查
compareTo() 用於對比的一個String 結果爲負、零或正,具體取決於String和自變量的字典順序。注意大寫和小寫不是相等的!
regionMatches() 這個String以及其他String的位置偏移,以及要比較的區域長度。覆蓋加入了“忽略大小寫”的特性 一個布爾結果,指出要對比的區域是否相同
startsWith() 可能以它開頭的String。覆蓋在自變量里加入了偏移 一個布爾結果,指出String是否以那個自變量開頭
endsWith() 可能是這個String後綴的一個String 一個布爾結果,指出自變量是不是一個後綴
indexOf(),lastIndexOf() 已覆蓋:char,char和起始索引,String,String和起始索引 若自變量未在這個String裏找到,則返回-1;否則返回自變量開始處的位置索引。lastIndexOf()可從終點開始回溯搜索
substring() 已覆蓋:起始索引,起始索引和結束索引 返回一個新的String對象,其中包含了指定的字符子集
concat() 想連結的String 返回一個新String對象,其中包含了原始String的字符,並在後面加上由自變量提供的字符
relpace() 要查找的老字符,要用它替換的新字符 返回一個新String對象,其中已完成了替換工作。若沒有找到相符的搜索項,就沿用老字串
toLowerCase(),toUpperCase() 無 返回一個新String對象,其中所有字符的大小寫形式都進行了統一。若不必修改,則沿用老字串
trim() 無 返回一個新的String對象,頭尾空白均已刪除。若毋需改動,則沿用老字串
valueOf() 已覆蓋:object,char[],char[]和偏移以及計數,boolean,char,int,long,float,double 返回一個String,其中包含自變量的一個字符表現形式
Intern() 無 爲每個獨一無二的字符順序都產生一個(而且只有一個)String句柄

可以看到,一旦有必要改變原來的內容,每個String方法都小心地返回了一個新的String對象。另外要注意的一個問題是,若內容不需要改變,則方法只返回指向原來那個String的一個句柄。這樣做可以節省存儲空間和系統開銷。
下面列出有關StringBuffer(字串緩衝)類的方法:

方法 自變量,覆蓋 用途

構建器 已覆蓋:默認,要創建的緩衝區長度,要根據它創建的String 新建一個StringBuffer對象
toString() 無 根據這個StringBuffer創建一個String
length() 無 StringBuffer中的字符數量
capacity() 無 返回目前分配的空間大小
ensureCapacity() 用於表示希望容量的一個整數 使StringBuffer容納至少希望的空間大小
setLength() 用於指示緩衝區內字串新長度的一個整數 縮短或擴充前一個字符串。如果是擴充,則用null值填充空隙
charAt() 表示目標元素所在位置的一個整數 返回位於緩衝區指定位置處的char
setCharAt() 代表目標元素位置的一個整數以及元素的一個新char值 修改指定位置處的值
getChars() 複製的起點和終點,要在其中複製的數組以及目標數組的一個索引 將char複製到一個外部數組。和String不同,這裏沒有getBytes()可供使用
append() 已覆蓋:Object,String,char[],特定偏移和長度的char[],boolean,char,int,long,float,double 將自變量轉換成一個字串,並將其追加到當前緩衝區的末尾。若有必要,同時增大緩衝區的長度
insert() 已覆蓋,第一個自變量代表開始插入的位置:Object,String,char[],boolean,char,int,long,float,double 第二個自變量轉換成一個字串,並插入當前緩衝區。插入位置在偏移區域的起點處。若有必要,同時會增大緩衝區的長度
reverse() 無 反轉緩衝內的字符順序

最常用的一個方法是append()。在計算包含了+和+=運算符的String表達式時,編譯器便會用到這個方法。insert()方法採用類似的形式。這兩個方法都能對緩衝區進行重要的操作,不需要另建新對象。

12.4.5 字串的特殊性
現在,大家已知道String類並非僅僅是Java提供的另一個類。String裏含有大量特殊的類。通過編譯器和特殊的覆蓋或過載運算符+和+=,可將引號字符串轉換成一個String。在本章中,大家已見識了剩下的一種特殊情況:用同志StringBuffer精心構造的“不可變”能力,以及編譯器中出現的一些有趣現象。

12.5 總結
由於Java中的所有東西都是句柄,而且由於每個對象都是在內存堆中創建的——只有不再需要的時候,纔會當作垃圾收集掉,所以對象的操作方式發生了變化,特別是在傳遞和返回對象的時候。舉個例子來說,在C和C++中,如果想在一個方法裏初始化一些存儲空間,可能需要請求用戶將那片存儲區域的地址傳遞進入方法。否則就必須考慮由誰負責清除那片區域。因此,這些方法的接口和對它們的理解就顯得要複雜一些。但在Java中,根本不必關心由誰負責清除,也不必關心在需要一個對象的時候它是否仍然存在。因爲系統會爲我們照料一切。我們的程序可在需要的時候創建一個對象。而且更進一步地,根本不必擔心那個對象的傳輸機制的細節:只需簡單地傳遞句柄即可。有些時候,這種簡化非常有價值,但另一些時候卻顯得有些多餘。
可從兩個方面認識這一機制的缺點:
(1) 肯定要爲額外的內存管理付出效率上的損失(儘管損失不大),而且對於運行所需的時間,總是存在一絲不確定的因素(因爲在內存不夠時,垃圾收集器可能會被強制採取行動)。對大多數應用來說,優點顯得比缺點重要,而且部分對時間要求非常苛刻的段落可以用native方法寫成(參見附錄A)。
(2) 別名處理:有時會不慎獲得指向同一個對象的兩個句柄。只有在這兩個句柄都假定指向一個“明確”的對象時,纔有可能產生問題。對這個問題,必須加以足夠的重視。而且應該儘可能地“克隆”一個對象,以防止另一個句柄被不希望的改動影響。除此以外,可考慮創建“不可變”對象,使它的操作能返回同種類型或不同種類型的一個新對象,從而提高程序的執行效率。但千萬不要改變原始對象,使對那個對象別名的其他任何方面都感覺不出變化。

有些人認爲Java的克隆是一個笨拙的傢伙,所以他們實現了自己的克隆方案(註釋⑤),永遠杜絕調用Object.clone()方法,從而消除了實現Cloneable和捕獲CloneNotSupportException違例的需要。這一做法是合理的,而且由於clone()在Java標準庫中很少得以支持,所以這顯然也是一種“安全”的方法。只要不調用Object.clone(),就不必實現Cloneable或者捕獲違例,所以那看起來也是能夠接受的。

⑤:Doug Lea特別重視這個問題,並把這個方法推薦給了我,他說只需爲每個類都創建一個名爲duplicate()的函數即可。

Java中一個有趣的關鍵字是byvalue(按值),它屬於那些“保留但未實現”的關鍵字之一。在理解了別名和克隆問題以後,大家可以想象byvalue最終有一天會在Java中用於實現一種自動化的本地副本。這樣做可以解決更多複雜的克隆問題,並使這種情況下的編寫的代碼變得更加簡單和健壯。

12.6 練習
(1) 創建一個myString類,在其中包含了一個String對象,以便用在構建器中用構建器的自變量對其進行初始化。添加一個toString()方法以及一個concatenate()方法,令其將一個String對象追加到我們的內部字串。在myString中實現clone()。創建兩個static方法,每個都取得一個myString x句柄作爲自己的自變量,並調用x.concatenate("test")。但在第二個方法中,請首先調用clone()。測試這兩個方法,觀察它們不同的結果。
(2) 創建一個名爲Battery(電池)的類,在其中包含一個int,用它表示電池的編號(採用獨一無二的標識符的形式)。接下來,創建一個名爲Toy的類,其中包含了一個Battery數組以及一個toString,用於打印出所有電池。爲Toy寫一個clone()方法,令其自動關閉所有Battery對象。克隆Toy並打印出結果,完成對它的測試。
(3) 修改CheckCloneable.java,使所有clone()方法都能捕獲CloneNotSupportException違例,而不是把它直接傳遞給調用者。
(4) 修改Compete.java,爲Thing2和Thing4類添加更多的成員對象,看看自己是否能判斷計時隨複雜性變化的規律——是一種簡單的線性關係,還是看起來更加複雜。
(5) 從Snake.java開始,創建Snake的一個深層複製版本。

 

英文版主頁 | 中文版主頁 | 詳細目錄 | 關於譯者

 
發佈了67 篇原創文章 · 獲贊 3 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章