java 傳遞和返回對象_第1頁
java 傳遞和返回對象_第2頁
java 傳遞和返回對象_第3頁
java 傳遞和返回對象_第4頁
java 傳遞和返回對象_第5頁
已閱讀5頁,還剩13頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

1、第 12 章 傳遞和返回對象 到目前為止,讀者應對對象的“傳遞”有了一個較為深刻的認識,記住實際傳遞 的只是一個句柄。在許多程序設計語言中,我們可用語言的“普通”方式到處傳遞對象,而且大多 數(shù)時候都不會遇到問題。但有些時候卻不得不采取一些非常做法,使得情況突然 變得稍微復雜起來(在C+中則是變得非常復雜)。Java亦不例外,我們十分有 必要準確認識在對象傳遞和賦值時所發(fā)生的一切。這正是本章的宗旨。若讀者是從某些特殊的程序設計環(huán)境中轉移過來的,那么一般都會問到:“Java 有指針嗎?”有些人認為指針的操作很困難,而且十分危險,所以一廂情愿地認 為它沒有好處。同時由于Java有如此好的口碑,所以應

2、該很輕易地免除自己以 前編程中的麻煩,其中不可能夾帶有指針這樣的“危險品”。然而準確地說Java 是有指針的!事實上,Java中每個對象(除基本數(shù)據(jù)類型以外)的標識符都屬 于指針的一種。但它們的使用受到了嚴格的限制和防范,不僅編譯器對它們有 “戒心”,運行期系統(tǒng)也不例外。或者換從另一個角度說,Java有指針,但沒 有傳統(tǒng)指針的麻煩。我曾一度將這種指針叫做“句柄”,但你可以把它想像成“安全指針”。和預備學校為學生提供的安全剪刀類似除非特別有意,否則 不會傷著自己,只不過有時要慢慢來,要習慣一些沉悶的工作。傳遞句柄 將句柄傳遞進入一個方法時,指向的仍然是相同的對象。一個簡單的實驗可以證 明這一點(

3、若執(zhí)行這個程序時有麻煩,請參考第 3 章 3.1.2 小節(jié)“賦值”):頁程序toString 方法會在打印語句里自動調用,而 PassHandles 直接從 Object 繼承, 沒有 toString 的重新定義。因此,這里會采用 toString 的 Object 版本,打印 出對象的類,接著是那個對象所在的位置(不是句柄,而是對象的實際存儲位置)。 輸出結果如下:p inside main(): PassHandles1653748h inside f() : PassHandles1653748可以看到,無論p還是h引用的都是同一個對象。這比復制一個新的PassHandles 對象有效

4、多了,使我們能將一個參數(shù)發(fā)給一個方法。但這樣做也帶來了另一個重 要的問題。別名問題“別名”意味著多個句柄都試圖指向同一個對象,就象前面的例子展示的那樣。 若有人向那個對象里寫入一點什么東西,就會產(chǎn)生別名問題。若其他句柄的所有 者不希望那個對象改變,恐怕就要失望了。這可用下面這個簡單的例子說明:頁程序對下面這行:Alias1 y = x; / Assign the handle它會新建一個 Alias1 句柄,但不是把它分配給由 new 創(chuàng)建的一個新鮮對象,而 是分配給一個現(xiàn)有的句柄。所以句柄 x 的內容即對象 x 指向的地址被分 配給y,所以無論x還是y都與相同的對象連接起來。這樣一來,一旦x

5、的i在 下述語句中增值:x.i+;y的i值也必然受到影響。從最終的輸出就可以看出:頁上程序此時最直接的一個解決辦法就是干脆不這樣做:不要有意將多個句柄指向同一個 作用域內的同一個對象。這樣做可使代碼更易理解和調試。然而,一旦準備將句 柄作為一個自變量或參數(shù)傳遞這是 Java 設想的正常方法別名問題就會 自動出現(xiàn),因為創(chuàng)建的本地句柄可能修改“外部對象”(在方法作用域之外創(chuàng)建 的對象)。下面是一個例子:544 頁程序輸出如下:x: 7Calling f(x)x: 8方法改變了自己的參數(shù)外部對象。一旦遇到這種情況,必須判斷它是否合理, 用戶是否愿意這樣,以及是不是會造成問題。通常,我們調用一個方法是

6、為了產(chǎn)生返回值,或者用它改變?yōu)槠湔{用方法的那個 對象的狀態(tài)(方法其實就是我們向那個對象“發(fā)一條消息”的方式)。很少需要 調用一個方法來處理它的參數(shù);這叫作利用方法的“副作用”(SideEffec t)。 所以倘若創(chuàng)建一個會修改自己參數(shù)的方法,必須向用戶明確地指出這一情況,并 警告使用那個方法可能會有的后果以及它的潛在威脅。由于存在這些混淆和缺 陷,所以應該盡量避免改變參數(shù)。若需在一個方法調用期間修改一個參數(shù),且不打算修改外部參數(shù),就應在自己的 方法內部制作一個副本,從而保護那個參數(shù)。本章的大多數(shù)內容都是圍繞這個問 題展開的。12.2 制作本地副本稍微總結一下: Java 中的所有自變量或參數(shù)傳

7、遞都是通過傳遞句柄進行的。也 就是說,當我們傳遞“一個對象”時,實際傳遞的只是指向位于方法外部的那個 對象的“一個句柄”。所以一旦要對那個句柄進行任何修改,便相當于修改外部 對象。此外:參數(shù)傳遞過程中會自動產(chǎn)生別名問題 不存在本地對象,只有本地句柄句柄有自己的作用域,而對象沒有對象的“存在時間”在 Java 里不是個問題 沒有語言上的支持(如常量)可防止對象被修改(以避免別名的副作用)若只是從對象中讀取信息,而不修改它,傳遞句柄便是自變量傳遞中最有效的一 種形式。這種做非常恰當;默認的方法一般也是最有效的方法。然而,有時仍需 將對象當作“本地的”對待,使我們作出的改變只影響一個本地副本,不會對

8、外 面的對象造成影響。許多程序設計語言都支持在方法內自動生成外部對象的一個 本地副本(注釋)。盡管Java不具備這種能力,但允許我們達到同樣的效果。:在C語言中,通??刂频氖巧倭繑?shù)據(jù)位,默認操作是按值傳遞。C+也必須 遵照這一形式,但按值傳遞對象并非肯定是一種有效的方式。此外,在C+中用 于支持按值傳遞的代碼也較難編寫,是件讓人頭痛的事情。按值傳遞 首先要解決術語的問題,最適合“按值傳遞”的看起來是自變量?!鞍粗祩鬟f” 以及它的含義取決于如何理解程序的運行方式。最常見的意思是獲得要傳遞的任 何東西的一個本地副本,但這里真正的問題是如何看待自己準備傳遞的東西。對 于“按值傳遞”的含義,目前存在兩

9、種存在明顯區(qū)別的見解:Java按值傳遞任何東西。若將基本數(shù)據(jù)類型傳遞進入一個方法,會明確得 到基本數(shù)據(jù)類型的一個副本。但若將一個句柄傳遞進入方法,得到的是句柄的副 本。所以人們認為“一切”都按值傳遞。當然,這種說法也有一個前提:句柄肯 定也會被傳遞。但 Java 的設計方案似乎有些超前,允許我們忽略(大多數(shù)時候) 自己處理的是一個句柄。也就是說,它允許我們將句柄假想成“對象”,因為在 發(fā)出方法調用時,系統(tǒng)會自動照管兩者間的差異。Java 主要按值傳遞(無自變量),但對象卻是按引用傳遞的。得到這個結 論的前提是句柄只是對象的一個“別名”,所以不考慮傳遞句柄的問題,而是直 接指出“我準備傳遞對象”

10、。由于將其傳遞進入一個方法時沒有獲得對象的一個 本地副本,所以對象顯然不是按值傳遞的。 Sun 公司似乎在某種程度上支持這一 見解,因為它“保留但未實現(xiàn)”的關鍵字之一便是byvalue (按值)。但沒人知 道那個關鍵字什么時候可以發(fā)揮作用。盡管存在兩種不同的見解,但其間的分歧歸根到底是由于對“句柄”的不同解釋 造成的。我打算在本書剩下的部分里回避這個問題。大家不久就會知道,這個問 題爭論下去其實是沒有意義的最重要的是理解一個句柄的傳遞會使調用者 的對象發(fā)生意外的改變??寺ο笕粜栊薷囊粋€對象,同時不想改變調用者的對象,就要制作該對象的一個本地副 本。這也是本地副本最常見的一種用途。若決定制作一

11、個本地副本,只需簡單地 使用clone()方法即可。Clone是“克隆”的意思,即制作完全一模一樣的副本。 這個方法在基礎類Object中定義成“protected”(受保護)模式。但在希望克 隆的任何衍生類中,必須將其覆蓋為“public”模式。例如,標準庫類Vector 覆蓋了 clone(),所以能為Vector調用clone(),如下所示: 547 頁程序 clone()方法產(chǎn)生了一個Object,后者必須立即重新造型為正確類型。這個例子 指出Vector的clone()方法不能自動嘗試克隆Vector內包含的每個對象 由 于別名問題,老的Vector和克隆的Vector都包含了相同的

12、對象。我們通常把這 種情況叫作“簡單復制”或者“淺層復制”,因為它只復制了一個對象的“表 面”部分。實際對象除包含這個“表面”以外,還包括句柄指向的所有對象,以 及那些對象又指向的其他所有對象,由此類推。這便是“對象網(wǎng)”或“對象關系 網(wǎng)”的由來。若能復制下所有這張網(wǎng),便叫作“全面復制”或者“深層復制”。在輸出中可看到淺層復制的結果,注意對v2采取的行動也會影響到v:548 頁上程序一般來說,由于不敢保證Vector里包含的對象是“可以克隆”(注釋)的, 所以最好不要試圖克隆那些對象。:“可以克隆”用英語講是cloneable,請留意Java庫中專門保留了這樣的 一個關鍵字。使類具有克隆能力 盡

13、管克隆方法是在所有類最基本的 Object 中定義的,但克隆仍然不會在每個類 里自動進行。這似乎有些不可思議,因為基礎類方法在衍生類里是肯定能用的。 但Java確實有點兒反其道而行之;如果想在一個類里使用克隆方法,唯一的辦 法就是專門添加一些代碼,以便保證克隆的正常進行。1. 使用 protected 時的技巧為避免我們創(chuàng)建的每個類都默認具有克隆能力,clone()方法在基礎類Object 里得到了“保留”(設為pro tec ted)。這樣造成的后果就是:對那些簡單地使 用一下這個類的客戶程序員來說,他們不會默認地擁有這個方法;其次,我們不 能利用指向基礎類的一個句柄來調用clone()(盡

14、管那樣做在某些情況下特別有 用,比如用多形性的方式克隆一系列對象)。在編譯期的時候,這實際是通知我 們對象不可克隆的一種方式而且最奇怪的是,Java庫中的大多數(shù)類都不能克隆。因此,假如我們執(zhí)行下述代碼:Integer x = new Integer(l);x = x.clone();那么在編譯期,就有一條討厭的錯誤消息彈出,告訴我們不可訪問clone() 因為 Integer 并沒有覆蓋它,而且它對 protected 版本來說是默認的)。但是,假若我們是在一個從Object衍生出來的類中(所有類都是從Object衍生 的),就有權調用Objec t.clone(),因為它是“ pro tec

15、 ted”,而且我們在一個 繼承器中?;A類clone()提供了一個有用的功能一一它進行的是對衍生類對象 的真正“按位”復制,所以相當于標準的克隆行動。然而,我們隨后需要將自己 的克隆操作設為public,否則無法訪問??傊?,克隆時要注意的兩個關鍵問題 是:幾乎肯定要調用super.clone(),以及注意將克隆設為public。有時還想在更深層的衍生類中覆蓋clone(),否則就直接使用我們的clone()(現(xiàn) 在已成為public),而那并不一定是我們所希望的(然而,由于Objec t.clone() 已制作了實際對象的一個副本,所以也有可能允許這種情況)。protected的技 巧在這里

16、只能用一次:首次從一個不具備克隆能力的類繼承,而且想使一個類變 成“能夠克隆”。而在從我們的類繼承的任何場合,clone()方法都是可以使用 的,因為Java不可能在衍生之后反而縮小方法的訪問范圍。換言之,一旦對象 變得可以克隆,從它衍生的任何東西都是能夠克隆的,除非使用特殊的機制(后 面討論)令其“關閉”克隆能力。2.實現(xiàn) Cloneable 接口為使一個對象的克隆能力功成圓滿,還需要做另一件事情:實現(xiàn)Cloneable接口。 這個接口使人稍覺奇怪,因為它是空的!interface Cloneable 之所以要實現(xiàn)這個空接口,顯然不是因為我們準備上溯造型成一個Cloneable, 以及調用它

17、的某個方法。有些人認為在這里使用接口屬于一種“欺騙”行為,因 為它使用的特性打的是別的主意,而非原來的意思。Cloneable in terface的實 現(xiàn)扮演了一個標記的角色,封裝到類的類型中。兩方面的原因促成了 Cloneable interface的存在。首先,可能有一個上溯造型 句柄指向一個基礎類型,而且不知道它是否真的能克隆那個對象。在這種情況下, 可用instanceof關鍵字(第11章有介紹)調查句柄是否確實同一個能克隆的對 象連接:if(myHandle instanceof Cloneable) / .第二個原因是考慮到我們可能不愿所有對象類型都能克隆。所以Object.cl

18、oneO 會驗證一個類是否真的是實現(xiàn)了 Cloneable接口。若答案是否定的,則“擲”出 一個CloneNotSupportedException違例。所以在一般情況下,我們必須將 “implement Cloneable 作為對克隆能力提供支持的一部分。12.2.4成功的克隆理解了實現(xiàn)clone()方法背后的所有細節(jié)后,便可創(chuàng)建出能方便復制的類,以便 提供了一個本地副本:550-551頁程序不管怎樣,clone()必須能夠訪問,所以必須將其設為public (公共的)。其次, 作為clone ()的初期行動,應調用clone ()的基礎類版本。這里調用的clone() 是Object內部預

19、先定義好的。之所以能調用它,是由于它具有pro tec ted (受 到保護的)屬性,所以能在衍生的類里訪問。Object.clone()會檢查原先的對象有多大,再為新對象騰出足夠多的內存,將所 有二進制位從原來的對象復制到新對象。這叫作“按位復制”,而且按一般的想 法,這個工作應該是由clone()方法來做的。但在Object.clone ()正式開始操作 前,首先會檢查一個類是否Cloneable,即是否具有克隆能力換言之,它是 否實現(xiàn)了 Cloneable接口。若未實現(xiàn),Objec t.clone ()就擲出一個 CloneNotSupportedException違例,指出我們不能克隆

20、它。因此,我們最好用 一個try-catch塊將對super.clone ()的調用代碼包圍(或圭寸裝)起來,試圖捕 獲一個應當永不出現(xiàn)的違例(因為這里確實已實現(xiàn)了 Cloneable接口)。 在LocalCopy中,兩個方法g()和f()揭示出兩種參數(shù)傳遞方法間的差異。其中, g()演示的是按引用傳遞,它會修改外部對象,并返回對那個外部對象的一個引 用。而f()是對自變量進行克隆,所以將其分離出來,并讓原來的對象保持獨立。 隨后,它繼續(xù)做它希望的事情。甚至能返回指向這個新對象的一個句柄,而且不 會對原來的對象產(chǎn)生任何副作用。注意下面這個多少有些古怪的語句:v = (MyObject)v.cl

21、one();它的作用正是創(chuàng)建一個本地副本。為避免被這樣的一個語句搞混淆,記住這種相 當奇怪的編碼形式在 Java 中是完全允許的,因為有一個名字的所有東西實際都 是一個句柄。所以句柄 v 用于克隆一個它所指向的副本,而且最終返回指向基礎 類型Object的一個句柄(因為它在Object.clone ()中是那樣被定義的),隨后 必須將其造型為正確的類型。在 main() 中,兩種不同參數(shù)傳遞方式的區(qū)別在于它們分別測試了一個不同的方 法。輸出結果如下:552 頁程序大家要記住這樣一個事實:Java對“是否等價”的測試并不對所比較對象的內 部進行檢查,從而核實它們的值是否相同。=和!=運算符只是簡

22、單地對比句柄的 內容。若句柄內的地址相同,就認為句柄指向同樣的對象,所以認為它們是“等 價”的。所以運算符真正檢測的是“由于別名問題,句柄是否指向同一個對 象?”O(jiān)bjec t.clone ()的效果調用Objec t.clone ()時,實際發(fā)生的是什么事情呢?當我們在自己的類里覆蓋 clone()時,什么東西對于super.clone ()來說是最關鍵的呢?根類中的clone() 方法負責建立正確的存儲容量,并通過“按位復制”將二進制位從原始對象中復 制到新對象的存儲空間。也就是說,它并不只是預留存儲空間以及復制一個對象 實際需要調查出欲復制之對象的準確大小,然后復制那個對象。由于所有這

23、些工作都是在由根類定義之clone()方法的內部代碼中進行的(根類并不知道要 從自己這里繼承出去什么),所以大家或許已經(jīng)猜到,這個過程需要用 RTTI 判 斷欲克隆的對象的實際大小。采取這種方式,clone()方法便可建立起正確數(shù)量 的存儲空間,并對那個類型進行正確的按位復制。不管我們要做什么,克隆過程的第一個部分通常都應該是調用 super.clone()。 通過進行一次準確的復制,這樣做可為后續(xù)的克隆進程建立起一個良好的基礎。 隨后,可采取另一些必要的操作,以完成最終的克隆。為確切了解其他操作是什么,首先要正確理解Objec t.clone ()為我們帶來了什 么。特別地,它會自動克隆所有

24、句柄指向的目標嗎?下面這個例子可完成這種形 式的檢測:553-554 頁程序一條Snake (蛇)由數(shù)段構成,每一段的類型都是Snake。所以,這是一個一段 段鏈接起來的列表。所有段都是以循環(huán)方式創(chuàng)建的,每做好一段,都會使第一個 構建器參數(shù)的值遞減,直至最終為零。而為給每段賦予一個獨一無二的標記,第 二個參數(shù)(一個Char)的值在每次循環(huán)構建器調用時都會遞增。increment() 方法的作用是循環(huán)遞增每個標記,使我們能看到發(fā)生的變化;而 toString 則循環(huán)打印出每個標記。輸出如下:554 頁中程序 這意味著只有第一段才是由Object.clone ()復制的,所以此時進行的是一種 “淺

25、層復制”。若希望復制整條蛇即進行“深層復制”必須在被覆蓋的 clone()里采取附加的操作。通??稍趶囊粋€能克隆的類里調用super.cloneO,以確保所有基礎類行動(包 括Object.clone()能夠進行。隨著是為對象內每個句柄都明確調用一個 clone();否則那些句柄會別名變成原始對象的句柄。構建器的調用也大致相同 首先構造基礎類,然后是下一個衍生的構建器以此類推,直到位于最深 層的衍生構建器。區(qū)別在于clone()并不是個構建器,所以沒有辦法實現(xiàn)自動克 隆。為了克隆,必須由自己明確進行??寺『铣蓪ο笤噲D深層復制合成對象時會遇到一個問題。必須假定成員對象中的clone()方法 也能

26、依次對自己的句柄進行深層復制,以此類推。這使我們的操作變得復雜。為 了能正常實現(xiàn)深層復制,必須對所有類中的代碼進行控制,或者至少全面掌握深 層復制中需要涉及的類,確保它們自己的深層復制能正確進行。下面這個例子總結了面對一個合成對象進行深層復制時需要做哪些事情:555-556 頁程序DepthReading和TemperatureReading非常相似;它們都只包含了基本數(shù)據(jù)類型。 所以clone()方法能夠非常簡單:調用super.clone()并返回結果即可。注意兩 個類使用的clone()代碼是完全一致的。OceanReading 是由 DepthReading 和 Temperature

27、Reading 對象合并而成的。為 了對其進行深層復制,clone()必須同時克隆OceanReading內的句柄。為達到這 個目標,super.clone ()的結果必須造型成一個OceanReading對象(以便訪問 depth 和 temperature 句柄)。用 Vector 進行深層復制 下面讓我們復習一下本章早些時候提出的Vector例子。這一次Int2類是可以克 隆的,所以能對Vector進行深層復制:557-558 頁程序Int3自Int2繼承而來,并添加了一個新的基本類型成員int j。大家也許認為 自己需要再次覆蓋clone(),以確保j得到復制,但實情并非如此。將Int

28、2的 clone()當作Int3的clone ()調用時,它會調用Object.clone(),判斷出當前操 作的是Int3,并復制Int3內的所有二進制位。只要沒有新增需要克隆的句柄, 對Object.clone()的一個調用就能完成所有必要的復制無論clone()是在層次結構多深的一級定義的。至此,大家可以總結出對Vector進行深層復制的先決條件:在克隆了 Vector 后,必須在其中遍歷,并克隆由Vec tor指向的每個對象。為了對Hash table (散 列表)進行深層復制,也必須采取類似的處理。這個例子剩余的部分顯示出克隆已實際進行證據(jù)就是在克隆了對象以后,可 以自由改變它,而原

29、來那個對象不受任何影響。通過序列化進行深層復制若研究一下第 10章介紹的那個 Java 1.1 對象序列化示例,可能發(fā)現(xiàn)若在一個對 象序列化以后再撤消對它的序列化,或者說進行裝配,那么實際經(jīng)歷的正是一個 “克隆”的過程。那么為什么不用序列化進行深層復制呢?下面這個例子通過計算執(zhí)行時間對比 了這兩種方法:559-560 頁程序其中,Thing2和Thing4包含了成員對象,所以需要進行一些深層復制。一個有 趣的地方是盡管 Serializable 類很容易設置,但在復制它們時卻要做多得多的 工作??寺∩婕暗酱罅康念愒O置工作,但實際的對象復制是相當簡單的。結果很 好地說明了一切。下面是幾次運行分別

30、得到的結果:的確561 頁上程序除了序列化和克隆之間巨大的時間差異以外,我們也注意到序列化技術的運行結 果并不穩(wěn)定,而克隆每一次花費的時間都是相同的。使克隆具有更大的深度若新建一個類,它的基礎類會默認為Object,并默認為不具備克隆能力(就象 在下一節(jié)會看到的那樣)。只要不明確地添加克隆能力,這種能力便不會自動產(chǎn) 生。但我們可以在任何層添加它,然后便可從那個層開始向下具有克隆能力。如 下所示:561-562 頁程序添加克隆能力之前,編譯器會阻止我們的克隆嘗試。一旦在Scientist里添加了 克隆能力,那么Scientist以及它的所有“后裔”都可以克隆。為什么有這個奇怪的設計 之所以感覺這

31、個方案的奇特,因為它事實上的確如此。也許大家會奇怪它為什么 要象這樣運行,而該方案背后的真正含義是什么呢?后面講述的是一個未獲證實 的故事一一大概是由于圍繞Java的許多買賣使其成為一種設計優(yōu)良的語言 但確實要花許多口舌才能講清楚這背后發(fā)生的所有事情。最初,Java只是作為一種用于控制硬件的語言而設計,與因特網(wǎng)并沒有絲毫聯(lián) 系。象這樣一類面向大眾的語言一樣,其意義在于程序員可以對任意一個對象進 行克隆。這樣一來,clone ()就放置在根類Object里面,但因為它是一種公用方 式,因而我們通常能夠對任意一個對象進行克隆??磥磉@是最靈活的方式了,畢 竟它不會帶來任何害處。正當Java看起來象一

32、種終級因特網(wǎng)程序設計語言的時候,情況卻發(fā)生了變化。 突然地,人們提出了安全問題,而且理所當然,這些問題與使用對象有關,我們 不愿望任何人克隆自己的保密對象。所以我們最后看到的是為原來那個簡單、直 觀的方案添加的大量補?。?clone()在Object里被設置成“protected”。必須 將其覆蓋,并使用“implement Cloneable”,同時解決違例的問題。只有在準備調用Object的clone()方法時,才沒有必要使用Cloneable接口, 因為那個方法會在運行期間得到檢查,以確保我們的類實現(xiàn)了 Cloneable。但為 了保持連貫性(而且由于Cloneable無論如何都是空的)

33、,最好還是由自己實現(xiàn) Cloneable。克隆的控制為消除克隆能力,大家也許認為只需將clone()方法簡單地設為private (私有) 即可,但這樣是行不通的,因為不能采用一個基礎類方法,并使其在衍生類中更 “私有”。所以事情并沒有這么簡單。此外,我們有必要控制一個對象是否能夠 克隆。對于我們設計的一個類,實際有許多種方案都是可以采取的:保持中立,不為克隆做任何事情。也就是說,盡管不可對我們的類克隆,但 從它繼承的一個類卻可根據(jù)實際情況決定克隆。只有Object.clone ()要對類中 的字段進行某些合理的操作時,才可以作這方面的決定。支持clone(),采用實現(xiàn)Cloneable (可

34、克隆)能力的標準操作,并覆蓋 clone()。在被覆蓋的clone()中,可調用super.clone(),并捕獲所有違例(這 樣可使clone ()不“擲”出任何違例)。有條件地支持克隆。若類容納了其他對象的句柄,而那些對象也許能夠克隆 (集合類便是這樣的一個例子),就可試著克隆擁有對方句柄的所有對象;如果它們“擲”出了違例,只需讓這些違例通過即可。舉個例子來說,假設有一個特 殊的Vector,它試圖克隆自己容納的所有對象。編寫這樣的一個Vector時,并 不知道客戶程序員會把什么形式的對象置入這個Vector中,所以并不知道它們 是否真的能夠克隆。不實現(xiàn)Cloneable(),但是將clo

35、ne()覆蓋成protected,使任何字段都具 有正確的復制行為。這樣一來,從這個類繼承的所有東西都能覆蓋clone(),并 調用super.clone()來產(chǎn)生正確的復制行為。注意在我們實現(xiàn)方案里,可以而且 應該調用super.clone() 即使那個方法本來預期的是一個Cloneable對象(否則會擲出一個違例),因為沒有人會在我們這種類型的對象上直接調用它。 它只有通過一個衍生類調用;對那個衍生類來說,如果要保證它正常工作,需實 現(xiàn) Cloneable。不實現(xiàn)Cloneable來試著防止克隆,并覆蓋clone(),以產(chǎn)生一個違例。為 使這一設想順利實現(xiàn),只有令從它衍生出來的任何類都調用

36、重新定義后的 clone()里的 suepr.clone()。將類設為final,從而防止克隆。若clone()尚未被我們的任何一個上級類 覆蓋,這一設想便不會成功。若已被覆蓋,那么再一次覆蓋它,并“擲”出一個 CloneNotSupportedException (克隆不支持)違例。為擔保克隆被禁止,將類設 為 final 是唯一的辦法。除此以外,一旦涉及保密對象或者遇到想對創(chuàng)建的對象 數(shù)量進行控制的其他情況,應該將所有構建器都設為private,并提供一個或更 多的特殊方法來創(chuàng)建對象。采用這種方式,這些方法就可以限制創(chuàng)建的對象數(shù)量 以及它們的創(chuàng)建條件一一一種特殊情況是第16章要介紹的sin

37、gle ton (獨子) 方案。下面這個例子總結了克隆的各種實現(xiàn)方法,然后在層次結構中將其“關閉”:564-565 頁程序第一個類Ordinary代表著大家在本書各處最常見到的類:不支持克隆,但在它 正式應用以后,卻也不禁止對其克隆。但假如有一個指向Ordinary對象的句柄, 而且那個對象可能是從一個更深的衍生類上溯造型來的,便不能判斷它到底能不 能克隆。WrongClone 類揭示了實現(xiàn)克隆的一種不正確途徑。它確實覆蓋了Object.clone(),并將那個方法設為public,但卻沒有實現(xiàn)Cloneable。所以一 旦發(fā)出對super.clone ()的調用(由于對Object.clon

38、e ()的一個調用造成的), 便會無情地擲出CloneNotSupportedException違例。在IsCloneable中,大家看到的才是進行克隆的各種正確行動:先覆蓋clone(), 并實現(xiàn)了 Cloneable。但是,這個clone()方法以及本例的另外幾個方法并不捕 獲CloneNotSupportedException違例,而是任由它通過,并傳遞給調用者。隨 后,調用者必須用一個try-catch代碼塊把它包圍起來。在我們自己的clone() 方法中,通常需要在clone ()內部捕獲CloneNotSupportedException違例,而 不是任由它通過。正如大家以后會理解

39、的那樣,對這個例子來說,讓它通過是最 正確的做法。類NoMore試圖按照Java設計者打算的那樣“關閉”克隆:在衍生類clone()中, 我們擲出 CloneNotSupportedException 違例o TryMore 類中的 clone()方法正確 地調用super.clone(),并解析成NoMore.cloneO,后者擲出一個違例并禁止克 隆。但在已被覆蓋的clone()方法中,假若程序員不遵守調用super.clone ()的“正 確”方法,又會出現(xiàn)什么情況呢?在BackOn中,大家可看到實際會發(fā)生什么。 這個類用一個獨立的方法duplicate()制作當前對象的一個副本,并在c

40、lone() 內部調用這個方法,而不是調用super.clone。違例永遠不會產(chǎn)生,而且新類 是可以克隆的。因此,我們不能依賴“擲”出一個違例的方法來防止產(chǎn)生一個可 克隆的類。唯一安全的方法在ReallyNoMore中得到了演示,它設為final,所 以不可繼承。這意味著假如clone()在final類中擲出了一個違例,便不能通過 繼承來進行修改,并可有效地禁止克隆(不能從一個擁有任意繼承級數(shù)的類中明 確調用Object.clone();只能調用super.clone(),它只可訪問直接基礎類)。 因此,只要制作一些涉及安全問題的對象,就最好把那些類設為 final。在類CheckClonea

41、ble中,我們看到的第一個類是tryToClone(),它能接納任何 Ordinary 對象,并用 instanceof 檢查它是否能夠克隆。若答案是肯定的,就將 對象造型成為一個IsCloneable,調用clone(),并將結果造型回Ordinary,最 后捕獲有可能產(chǎn)生的任何違例。請注意用運行期類型鑒定(見第11章)打印出 類名,使自己看到發(fā)生的一切情況。在main()中,我們創(chuàng)建了不同類型的Ordinary對象,并在數(shù)組定義中上溯造型 成為Ordinary。在這之后的頭兩行代碼創(chuàng)建了一個純粹的Ordinary對象,并試 圖對其克隆。然而,這些代碼不會得到編譯,因為clone ()是Ob

42、ject中的一個 pro tec ted (受到保護的)方法。代碼剩余的部分將遍歷數(shù)組,并試著克隆每個 對象,分別報告它們的成功或失敗。輸出如下:567-568 頁程序 總之,如果希望一個類能夠克隆,那么:實現(xiàn) Cloneable 接口覆蓋 clone()在自己的 clone ()中調用 super.clone()在自己的clone ()中捕獲違例 這一系列步驟能達到最理想的效果。副本構建器 克隆看起來要求進行非常復雜的設置,似乎還該有另一種替代方案。一個辦法是 制作特殊的構建器,令其負責復制一個對象。在C+中,這叫作“副本構建器”。 剛開始的時候,這好象是一種非常顯然的解決方案(如果你是C+

43、程序員,這個 方法就更顯親切)。下面是一個實際的例子:568-571 頁程序這個例子第一眼看上去顯得有點奇怪。不同水果的質量肯定有所區(qū)別,但為什么 只是把代表那些質量的數(shù)據(jù)成員直接置入Fruit (水果)類?有兩方面可能的原 因。第一個是我們可能想簡便地插入或修改質量。注意Fruit有一個protected (受到保護的)addQualities()方法,它允許衍生類來進行這些插入或修改操作 (大家或許會認為最合乎邏輯的做法是在Fruit中使用一個protected構建器, 用它獲取 FruitQualities 參數(shù),但構建器不能繼承,所以不可在第二級或級數(shù) 更深的類中使用它)。通過將水果的

44、質量置入一個獨立的類,可以得到更大的靈 活性,其中包括可以在特定 Fruit 對象的存在期間中途更改質量。之所以將 FruitQualities 設為一個獨立的對象,另一個原因是考慮到我們有時 希望添加新的質量,或者通過繼承與多形性改變行為。注意對 GreenZebra 來說 (這實際是西紅柿的一類我已栽種成功,它們簡直令人難以置信),構建器 會調用addQualities(),并為其傳遞一個ZebraQualities對象。該對象是從 FruitQualities 衍生出來的,所以能與基礎類中的 FruitQualities 句柄聯(lián)系在 一起。當然,一旦GreenZebra使用FruitQu

45、alities,就必須將其下溯造型成為 正確的類型(就象evaluate()中展示的那樣),但它肯定知道類型是 ZebraQualities。大家也看到有一個Seed (種子)類,F(xiàn)ruit (大家都知道,水果含有自己的種子) 包含了一個 Seed 數(shù)組。最后,注意每個類都有一個副本構建器,而且每個副本構建器都必須關心為基礎 類和成員對象調用副本構建器的問題,從而獲得“深層復制”的效果。對副本構 建器的測試是在CopyCons true tor類內進行的。方法ripen()需要獲取一個 Tomato 參數(shù),并對其執(zhí)行副本構建工作,以便復制對象: t = new Tomato(t);而slice

46、 ()需要獲取一個更常規(guī)的Fruit對象,而且對它進行復制:f = new Fruit(f);它們都在main()中伴隨不同種類的Fruit進行測試。下面是輸出結果:572 頁上程序 從中可以看出一個問題。在slice()內部對Toma to進行了副本構建工作以后, 結果便不再是一個Toma to對象,而只是一個Frui t。它已丟失了作為一個Toma to (西紅柿)的所有特征。此外,如果采用一個GreenZebra, ripen()和slice() 會把它分別轉換成一個Toma to和一個Frui t。所以非常不幸,假如想制作對象 的一個本地副本,Java中的副本構建器便不是特別適合我們。

47、1.為什么在C+的作用比在Java中大?副本構建器是C+的一個基本構成部分,因為它能自動產(chǎn)生對象的一個本地副 本。但前面的例子確實證明了它不適合在Java中使用,為什么呢?在Java中, 我們操控的一切東西都是句柄,而在C+中,卻可以使用類似于句柄的東西,也 能直接傳遞對象。這時便要用到C+的副本構建器:只要想獲得一個對象,并按 值傳遞它,就可以復制對象。所以它在C+里能很好地工作,但應注意這套機制 在 Java 里是很不通的,所以不要用它。只讀類盡管在一些特定的場合,由clone()產(chǎn)生的本地副本能夠獲得我們希望的結果, 但程序員(方法的作者)不得不親自禁止別名處理的副作用。假如想制作一個庫

48、, 令其具有常規(guī)用途,但卻不能擔保它肯定能在正確的類中得以克隆,這時又該怎 么辦呢?更有可能的一種情況是,假如我們想讓別名發(fā)揮積極的作用禁止不 必要的對象復制但卻不希望看到由此造成的副作用,那么又該如何處理呢? 一個辦法是創(chuàng)建“不變對象”,令其從屬于只讀類??啥x一個特殊的類,使其 中沒有任何方法能造成對象內部狀態(tài)的改變。在這樣的一個類中,別名處理是沒 有問題的。因為我們只能讀取內部狀態(tài),所以當多處代碼都讀取相同的對象時, 不會出現(xiàn)任何副作用。作為“不變對象”一個簡單例子,Java的標準庫包含了 “圭寸裝器”(wrapper) 類,可用于所有基本數(shù)據(jù)類型。大家可能已發(fā)現(xiàn)了這一點,如果想在一個象

49、 Vector (只采用Object句柄)這樣的集合里保存一個int數(shù)值,可以將這個int 封裝到標準庫的Integer類內部。如下所示:573 頁中程序Integer類(以及基本的“封裝器”類)用簡單的形式實現(xiàn)了“不變性”:它們 沒有提供可以修改對象的方法。若確實需要一個容納了基本數(shù)據(jù)類型的對象,并想對基本數(shù)據(jù)類型進行修改,就 必須親自創(chuàng)建它們。幸運的是,操作非常簡單:574 頁程序注意 n 在這里簡化了我們的編碼。 若默認的初始化為零已經(jīng)足夠(便不需要構建器),而且不用考慮把它打印出來(便不需要toString),那么IntValue甚至還能更加簡單。如下所示:class IntValue

50、 int n; 將元素取出來,再對其進行造型,這多少顯得有些笨拙,但那是Vector的問題, 不是IntValue的錯。創(chuàng)建只讀類 完全可以創(chuàng)建自己的只讀類,下面是個簡單的例子:575頁程序所有數(shù)據(jù)都設為private,可以看到?jīng)]有任何public方法對數(shù)據(jù)作出修改。事 實上,確實需要修改一個對象的方法是quadruple。,但它的作用是新建一個 Immutablel對象,初始對象則是原圭寸未動的。方法f()需要取得一個Immutablel對象,并對其采取不同的操作,而main()的 輸出顯示出沒有對x作任何修改。因此,x對象可別名處理許多次,不會造成任 何傷害,因為根據(jù)Immutable1類

51、的設計,它能保證對象不被改動?!耙怀刹蛔儭钡谋锥?從表面看,不變類的建立似乎是一個好方案。但是,一旦真的需要那種新類型的 一個修改的對象,就必須辛苦地進行新對象的創(chuàng)建工作,同時還有可能涉及更頻 繁的垃圾收集。對有些類來說,這個問題并不是很大。但對其他類來說(比如 St ring類),這一方案的代價顯得太高了。為解決這個問題,我們可以創(chuàng)建一個“同志”類,并使其能夠修改。以后只要涉 及大量的修改工作,就可換為使用能修改的同志類。完事以后,再切換回不可變 的類。因此,上例可改成下面這個樣子:577頁程序和往常一樣,Immutable2包含的方法保留了對象不可變的特征,只要涉及修改, 就創(chuàng)建新的對象。

52、完成這些操作的是add()和multiply ()方法。同志類叫作 Mut able,它也含有add()和mul tiply ()方法。但這些方法能夠修改Mu table對 象,而不是新建一個。除此以外,Mutable的一個方法可用它的數(shù)據(jù)產(chǎn)生一個 Immutable2對象,反之亦然。兩個靜態(tài)方法modify1()和modify2()揭示出獲得同樣結果的兩種不同方法。在 modify1()中,所有工作都是在Immutable2類中完成的,我們可看到在進程中創(chuàng) 建了四個新的Immutable2對象(而且每次重新分配了 val,前一個對象就成為 垃圾)。在方法modify2 ()中,可看到它的第一

53、個行動是獲取Immutable2 y,然后從中生 成一個Mutable (類似于前面對clone ()的調用,但這一次創(chuàng)建了一個不同類型 的對象)。隨后,用Mu table對象進行大量修改操作,同時用不著新建許多對象。 最后,它切換回Immut able2。在這里,我們只創(chuàng)建了兩個新對象(Mu table和 Immutable2的結果),而不是四個。這一方法特別適合在下述場合應用:需要不可變的對象,而且經(jīng)常需要進行大量修改,或者創(chuàng)建新的不變對象代價太高不變字串請觀察下述代碼:577-578 頁程序q 傳遞進入 upcase() 時,它實際是 q 的句柄的一個副本。該句柄連接的對象實際 只在一個

54、統(tǒng)一的物理位置處。句柄四處傳遞的時候,它的句柄會得到復制。若觀察對upcase ()的定義,會發(fā)現(xiàn)傳遞進入的句柄有一個名字s,而且該名字只 有在upcase ()執(zhí)行期間才會存在。upcase ()完成后,本地句柄s便會消失,而 upcase()返回結果一一還是原來那個字串,只是所有字符都變成了大寫。當然, 它返回的實際是結果的一個句柄。但它返回的句柄最終是為一個新對象的,同時 原來的 q 并未發(fā)生變化。所有這些是如何發(fā)生的呢?1. 隱式常數(shù)若使用下述語句:String s = asdf;String x = Stringer.upcase(s);那么真的希望upcase()方法改變自變量或者

55、參數(shù)嗎?我們通常是不愿意的,因 為作為提供給方法的一種信息,自變量一般是拿給代碼的讀者看的,而不是讓他 們修改。這是一個相當重要的保證,因為它使代碼更易編寫和理解。為了在C+中實現(xiàn)這一保證,需要一個特殊關鍵字的幫助:const。利用這個關 鍵字,程序員可以保證一個句柄(C+叫“指針”或者“引用”)不會被用來修 改原始的對象。但這樣一來,C+程序員需要用心記住在所有地方都使用const。 這顯然易使人混淆,也不容易記住。2. 覆蓋+和 StringBuffer 利用前面提到的技術, String 類的對象被設計成“不可變”。若查閱聯(lián)機文檔 中關于St ring類的內容(本章稍后還要總結它),就會

56、發(fā)現(xiàn)類中能夠修改St ring 的每個方法實際都創(chuàng)建和返回了一個嶄新的St ring對象,新對象里包含了修改 過的信息原來的St ring是原圭寸未動的。因此,Java里沒有與C+的cons t對應的特性可用來讓編譯器支持對象的不可變能力。若想獲得這一能力,可以自 行設置,就象St ring那樣。由于St ring對象是不可變的,所以能夠根據(jù)情況對一個特定的St ring進行多次 別名處理。因為它是只讀的,所以一個句柄不可能會改變一些會影響其他句柄的 東西。因此,只讀對象可以很好地解決別名問題。通過修改產(chǎn)生對象的一個嶄新版本,似乎可以解決修改對象時的所有問題,就象 St ring那樣。但對某些

57、操作來講,這種方法的效率并不高。一個典型的例子便 是為St ring對象覆蓋的運算符“ + ”?!案采w”意味著在與一個特定的類使用 時,它的含義已發(fā)生了變化(用于St ring的“ + ”和“+二”是Java中能被覆蓋 的唯一運算符,Java不允許程序員覆蓋其他任何運算符注釋)。:C+允許程序員隨意覆蓋運算符。由于這通常是一個復雜的過程(參見Thinking in C+,Prentice-Hall 于 1995 年出版),所以 Java 的設計者認定它是一種“糟糕”的特性,決定不在Java中采用。但具有諷剌意味的是, 運算符的覆蓋在Java中要比在C+中容易得多。針對St ring對象使用時,

58、“ + ”允許我們將不同的字串連接起來:頁中程序可以想象出它“可能”是如何工作的:字串a(chǎn)bc可以有一個方法append(),它 新建了一個字串,其中包含abc以及foo的內容;這個新字串然后再創(chuàng)建另一 個新字串,在其中添加def;以此類推。這一設想是行得通的,但它要求創(chuàng)建大量字串對象。盡管最終的目的只是獲得包 含了所有內容的一個新字串,但中間卻要用到大量字串對象,而且要不斷地進行 垃圾收集。我懷疑Java的設計者是否先試過種方法(這是軟件開發(fā)的一個教訓 除非自己試試代碼,并讓某些東西運行起來,否則不可能真正了解系統(tǒng))。 我還懷疑他們是否早就發(fā)現(xiàn)這樣做獲得的性能是不能接受的。解決的方法是象前面介

59、紹的那樣制作一個可變的同志類。對字串來說,這個同志 類叫作StringBuffer,編譯器可以自動創(chuàng)建一個StringBuffer,以便計算特定 的表達式,特別是面向St ring對象應用覆蓋過的運算符+和+二時。下面這個例子 可以解決這個問題:頁程序創(chuàng)建字串s時,編譯器做的工作大致等價于后面使用sb的代碼創(chuàng)建一個 StringBuffer,并用append ()將新字符直接加入StringBuffer對象(而不是每 次都產(chǎn)生新對象)。盡管這樣做更有效,但不值得每次都創(chuàng)建象abc 和def 這樣的引號字串,編譯器會把它們都轉換成St ring對象。所以盡管St ringBuffer 提供了更高

60、的效率,但會產(chǎn)生比我們希望的多得多的對象。String 和 StringBuffer 類這里總結一下同時適用于St ring和St ringBuffer的方法,以便對它們相互間的 溝通方式有一個印象。這些表格并未把每個單獨的方法都包括進去,而是包含了 與本次討論有重要關系的方法。那些已被覆蓋的方法用單獨一行總結。 首先總結 String 類的各種方法: 方法 自變量,覆蓋 用途 構建器 已被覆蓋:默認, String, StringBuffer, char 數(shù)組, byte 數(shù)組 創(chuàng)建 String 對象leng th()無St ring中的字符數(shù)量 charAt() int Index 位于

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經(jīng)權益所有人同意不得將文件中的內容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論