?? chapter12.htm
字號:
<br>
567-568頁程序<br>
<br>
總之,如果希望一個類能夠克隆,那么:<br>
(1) 實現(xiàn)Cloneable接口<br>
(2) 覆蓋clone()<br>
(3) 在自己的clone()中調(diào)用super.clone()<br>
(4) 在自己的clone()中捕獲違例<br>
這一系列步驟能達到最理想的效果。<br>
<br>
12.3.1 副本構(gòu)建器<br>
克隆看起來要求進行非常復(fù)雜的設(shè)置,似乎還該有另一種替代方案。一個辦法是制作特殊的構(gòu)建器,令其負責復(fù)制一個對象。在C++中,這叫作“副本構(gòu)建器”。剛開始的時候,這好象是一種非常顯然的解決方案(如果你是C++程序員,這個方法就更顯親切)。下面是一個實際的例子:<br>
<br>
568-571頁程序<br>
<br>
這個例子第一眼看上去顯得有點奇怪。不同水果的質(zhì)量肯定有所區(qū)別,但為什么只是把代表那些質(zhì)量的數(shù)據(jù)成員直接置入Fruit(水果)類?有兩方面可能的原因。第一個是我們可能想簡便地插入或修改質(zhì)量。注意Fruit有一個protected(受到保護的)addQualities()方法,它允許衍生類來進行這些插入或修改操作(大家或許會認為最合乎邏輯的做法是在Fruit中使用一個protected構(gòu)建器,用它獲取FruitQualities參數(shù),但構(gòu)建器不能繼承,所以不可在第二級或級數(shù)更深的類中使用它)。通過將水果的質(zhì)量置入一個獨立的類,可以得到更大的靈活性,其中包括可以在特定Fruit對象的存在期間中途更改質(zhì)量。<br>
之所以將FruitQualities設(shè)為一個獨立的對象,另一個原因是考慮到我們有時希望添加新的質(zhì)量,或者通過繼承與多形性改變行為。注意對GreenZebra來說(這實際是西紅柿的一類——我已栽種成功,它們簡直令人難以置信),構(gòu)建器會調(diào)用addQualities(),并為其傳遞一個ZebraQualities對象。該對象是從FruitQualities衍生出來的,所以能與基礎(chǔ)類中的FruitQualities句柄聯(lián)系在一起。當然,一旦GreenZebra使用FruitQualities,就必須將其下溯造型成為正確的類型(就象evaluate()中展示的那樣),但它肯定知道類型是ZebraQualities。<br>
大家也看到有一個Seed(種子)類,F(xiàn)ruit(大家都知道,水果含有自己的種子)包含了一個Seed數(shù)組。<br>
最后,注意每個類都有一個副本構(gòu)建器,而且每個副本構(gòu)建器都必須關(guān)心為基礎(chǔ)類和成員對象調(diào)用副本構(gòu)建器的問題,從而獲得“深層復(fù)制”的效果。對副本構(gòu)建器的測試是在CopyConstructor類內(nèi)進行的。方法ripen()需要獲取一個Tomato參數(shù),并對其執(zhí)行副本構(gòu)建工作,以便復(fù)制對象:<br>
t = new Tomato(t);<br>
而slice()需要獲取一個更常規(guī)的Fruit對象,而且對它進行復(fù)制:<br>
f = new Fruit(f);<br>
它們都在main()中伴隨不同種類的Fruit進行測試。下面是輸出結(jié)果:<br>
<br>
572頁上程序<br>
<br>
從中可以看出一個問題。在slice()內(nèi)部對Tomato進行了副本構(gòu)建工作以后,結(jié)果便不再是一個Tomato對象,而只是一個Fruit。它已丟失了作為一個Tomato(西紅柿)的所有特征。此外,如果采用一個GreenZebra,ripen()和slice()會把它分別轉(zhuǎn)換成一個Tomato和一個Fruit。所以非常不幸,假如想制作對象的一個本地副本,Java中的副本構(gòu)建器便不是特別適合我們。<br>
<br>
1. 為什么在C++的作用比在Java中大?<br>
副本構(gòu)建器是C++的一個基本構(gòu)成部分,因為它能自動產(chǎn)生對象的一個本地副本。但前面的例子確實證明了它不適合在Java中使用,為什么呢?在Java中,我們操控的一切東西都是句柄,而在C++中,卻可以使用類似于句柄的東西,也能直接傳遞對象。這時便要用到C++的副本構(gòu)建器:只要想獲得一個對象,并按值傳遞它,就可以復(fù)制對象。所以它在C++里能很好地工作,但應(yīng)注意這套機制在Java里是很不通的,所以不要用它。<br>
<br>
12.4 只讀類<br>
盡管在一些特定的場合,由clone()產(chǎn)生的本地副本能夠獲得我們希望的結(jié)果,但程序員(方法的作者)不得不親自禁止別名處理的副作用。假如想制作一個庫,令其具有常規(guī)用途,但卻不能擔保它肯定能在正確的類中得以克隆,這時又該怎么辦呢?更有可能的一種情況是,假如我們想讓別名發(fā)揮積極的作用——禁止不必要的對象復(fù)制——但卻不希望看到由此造成的副作用,那么又該如何處理呢?<br>
一個辦法是創(chuàng)建“不變對象”,令其從屬于只讀類。可定義一個特殊的類,使其中沒有任何方法能造成對象內(nèi)部狀態(tài)的改變。在這樣的一個類中,別名處理是沒有問題的。因為我們只能讀取內(nèi)部狀態(tài),所以當多處代碼都讀取相同的對象時,不會出現(xiàn)任何副作用。<br>
作為“不變對象”一個簡單例子,Java的標準庫包含了“封裝器”(wrapper)類,可用于所有基本數(shù)據(jù)類型。大家可能已發(fā)現(xiàn)了這一點,如果想在一個象Vector(只采用Object句柄)這樣的集合里保存一個int數(shù)值,可以將這個int封裝到標準庫的Integer類內(nèi)部。如下所示:<br>
<br>
573頁中程序<br>
<br>
Integer類(以及基本的“封裝器”類)用簡單的形式實現(xiàn)了“不變性”:它們沒有提供可以修改對象的方法。<br>
若確實需要一個容納了基本數(shù)據(jù)類型的對象,并想對基本數(shù)據(jù)類型進行修改,就必須親自創(chuàng)建它們。幸運的是,操作非常簡單:<br>
<br>
573-574頁程序<br>
<br>
注意n在這里簡化了我們的編碼。<br>
若默認的初始化為零已經(jīng)足夠(便不需要構(gòu)建器),而且不用考慮把它打印出來(便不需要toString),那么IntValue甚至還能更加簡單。如下所示:<br>
class IntValue { int n; }<br>
將元素取出來,再對其進行造型,這多少顯得有些笨拙,但那是Vector的問題,不是IntValue的錯。<br>
<br>
12.4.1 創(chuàng)建只讀類<br>
完全可以創(chuàng)建自己的只讀類,下面是個簡單的例子:<br>
<br>
574-575頁程序<br>
<br>
所有數(shù)據(jù)都設(shè)為private,可以看到?jīng)]有任何public方法對數(shù)據(jù)作出修改。事實上,確實需要修改一個對象的方法是quadruple(),但它的作用是新建一個Immutable1對象,初始對象則是原封未動的。<br>
方法f()需要取得一個Immutable1對象,并對其采取不同的操作,而main()的輸出顯示出沒有對x作任何修改。因此,x對象可別名處理許多次,不會造成任何傷害,因為根據(jù)Immutable1類的設(shè)計,它能保證對象不被改動。<br>
<br>
12.4.2 “一成不變”的弊端<br>
從表面看,不變類的建立似乎是一個好方案。但是,一旦真的需要那種新類型的一個修改的對象,就必須辛苦地進行新對象的創(chuàng)建工作,同時還有可能涉及更頻繁的垃圾收集。對有些類來說,這個問題并不是很大。但對其他類來說(比如String類),這一方案的代價顯得太高了。<br>
為解決這個問題,我們可以創(chuàng)建一個“同志”類,并使其能夠修改。以后只要涉及大量的修改工作,就可換為使用能修改的同志類。完事以后,再切換回不可變的類。<br>
因此,上例可改成下面這個樣子:<br>
<br>
575-577頁程序<br>
<br>
和往常一樣,Immutable2包含的方法保留了對象不可變的特征,只要涉及修改,就創(chuàng)建新的對象。完成這些操作的是add()和multiply()方法。同志類叫作Mutable,它也含有add()和multiply()方法。但這些方法能夠修改Mutable對象,而不是新建一個。除此以外,Mutable的一個方法可用它的數(shù)據(jù)產(chǎn)生一個Immutable2對象,反之亦然。<br>
兩個靜態(tài)方法modify1()和modify2()揭示出獲得同樣結(jié)果的兩種不同方法。在modify1()中,所有工作都是在Immutable2類中完成的,我們可看到在進程中創(chuàng)建了四個新的Immutable2對象(而且每次重新分配了val,前一個對象就成為垃圾)。<br>
在方法modify2()中,可看到它的第一個行動是獲取Immutable2 y,然后從中生成一個Mutable(類似于前面對clone()的調(diào)用,但這一次創(chuàng)建了一個不同類型的對象)。隨后,用Mutable對象進行大量修改操作,同時用不著新建許多對象。最后,它切換回Immutable2。在這里,我們只創(chuàng)建了兩個新對象(Mutable和Immutable2的結(jié)果),而不是四個。<br>
這一方法特別適合在下述場合應(yīng)用:<br>
(1) 需要不可變的對象,而且<br>
(2) 經(jīng)常需要進行大量修改,或者<br>
(3) 創(chuàng)建新的不變對象代價太高<br>
<br>
12.4.3 不變字串<br>
請觀察下述代碼:<br>
<br>
577-578頁程序<br>
<br>
q傳遞進入upcase()時,它實際是q的句柄的一個副本。該句柄連接的對象實際只在一個統(tǒng)一的物理位置處。句柄四處傳遞的時候,它的句柄會得到復(fù)制。<br>
若觀察對upcase()的定義,會發(fā)現(xiàn)傳遞進入的句柄有一個名字s,而且該名字只有在upcase()執(zhí)行期間才會存在。upcase()完成后,本地句柄s便會消失,而upcase()返回結(jié)果——還是原來那個字串,只是所有字符都變成了大寫。當然,它返回的實際是結(jié)果的一個句柄。但它返回的句柄最終是為一個新對象的,同時原來的q并未發(fā)生變化。所有這些是如何發(fā)生的呢?<br>
<br>
1. 隱式常數(shù)<br>
若使用下述語句:<br>
String s = "asdf";<br>
String x = Stringer.upcase(s);<br>
那么真的希望upcase()方法改變自變量或者參數(shù)嗎?我們通常是不愿意的,因為作為提供給方法的一種信息,自變量一般是拿給代碼的讀者看的,而不是讓他們修改。這是一個相當重要的保證,因為它使代碼更易編寫和理解。<br>
為了在C++中實現(xiàn)這一保證,需要一個特殊關(guān)鍵字的幫助:const。利用這個關(guān)鍵字,程序員可以保證一個句柄(C++叫“指針”或者“引用”)不會被用來修改原始的對象。但這樣一來,C++程序員需要用心記住在所有地方都使用const。這顯然易使人混淆,也不容易記住。<br>
<br>
2. 覆蓋"+"和StringBuffer<br>
利用前面提到的技術(shù),String類的對象被設(shè)計成“不可變”。若查閱聯(lián)機文檔中關(guān)于String類的內(nèi)容(本章稍后還要總結(jié)它),就會發(fā)現(xiàn)類中能夠修改String的每個方法實際都創(chuàng)建和返回了一個嶄新的String對象,新對象里包含了修改過的信息——原來的String是原封未動的。因此,Java里沒有與C++的const對應(yīng)的特性可用來讓編譯器支持對象的不可變能力。若想獲得這一能力,可以自行設(shè)置,就象String那樣。<br>
由于String對象是不可變的,所以能夠根據(jù)情況對一個特定的String進行多次別名處理。因為它是只讀的,所以一個句柄不可能會改變一些會影響其他句柄的東西。因此,只讀對象可以很好地解決別名問題。<br>
通過修改產(chǎn)生對象的一個嶄新版本,似乎可以解決修改對象時的所有問題,就象String那樣。但對某些操作來講,這種方法的效率并不高。一個典型的例子便是為String對象覆蓋的運算符“+”。“覆蓋”意味著在與一個特定的類使用時,它的含義已發(fā)生了變化(用于String的“+”和“+=”是Java中能被覆蓋的唯一運算符,Java不允許程序員覆蓋其他任何運算符——注釋④)。<br>
<br>
④:C++允許程序員隨意覆蓋運算符。由于這通常是一個復(fù)雜的過程(參見《Thinking
in C++》,Prentice-Hall于1995年出版),所以Java的設(shè)計者認定它是一種“糟糕”的特性,決定不在Java中采用。但具有諷剌意味的是,運算符的覆蓋在Java中要比在C++中容易得多。<br>
<br>
針對String對象使用時,“+”允許我們將不同的字串連接起來:<br>
<br>
579頁中程序<br>
<br>
可以想象出它“可能”是如何工作的:字串"abc"可以有一個方法append(),它新建了一個字串,其中包含"abc"以及foo的內(nèi)容;這個新字串然后再創(chuàng)建另一個新字串,在其中添加"def";以此類推。<br>
這一設(shè)想是行得通的,但它要求創(chuàng)建大量字串對象。盡管最終的目的只是獲得包含了所有內(nèi)容的一個新字串,但中間卻要用到大量字串對象,而且要不斷地進行垃圾收集。我懷疑Java的設(shè)計者是否先試過種方法(這是軟件開發(fā)的一個教訓(xùn)——除非自己試試代碼,并讓某些東西運行起來,否則不可能真正了解系統(tǒng))。我還懷疑他們是否早就發(fā)現(xiàn)這樣做獲得的性能是不能接受的。<br>
解決的方法是象前面介紹的那樣制作一個可變的同志類。對字串來說,這個同志類叫作StringBuffer,編譯器可以自動創(chuàng)建一個StringBuffer,以便計算特定的表達式,特別是面向String對象應(yīng)用覆蓋過的運算符+和+=時。下面這個例子可以解決這個問題:<br>
?? 快捷鍵說明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -