?? chapter12.htm
字號:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<title>Thinking in Java | Chinese Version by Trans Bot</title>
<meta name="Microsoft Theme" content="inmotion 111, default"></head>
<body background="../_themes/inmotion/inmtextb.gif" tppabs="http://member.netease.com/%7etransbot/Thinking%20in%20Java/_themes/inmotion/inmtextb.gif" bgcolor="#FFFFCC" text="#000000" link="#800000" vlink="#996633" alink="#FF3399">
<p>第12章 傳遞和返回對象<br>
<br>
到目前為止,讀者應(yīng)對對象的“傳遞”有了一個較為深刻的認識,記住實際傳遞的只是一個句柄。<br>
在許多程序設(shè)計語言中,我們可用語言的“普通”方式到處傳遞對象,而且大多數(shù)時候都不會遇到問題。但有些時候卻不得不采取一些非常做法,使得情況突然變得稍微復(fù)雜起來(在C++中則是變得非常復(fù)雜)。Java亦不例外,我們十分有必要準確認識在對象傳遞和賦值時所發(fā)生的一切。這正是本章的宗旨。<br>
若讀者是從某些特殊的程序設(shè)計環(huán)境中轉(zhuǎn)移過來的,那么一般都會問到:“Java有指針嗎?”有些人認為指針的操作很困難,而且十分危險,所以一廂情愿地認為它沒有好處。同時由于Java有如此好的口碑,所以應(yīng)該很輕易地免除自己以前編程中的麻煩,其中不可能夾帶有指針這樣的“危險品”。然而準確地說,Java是有指針的!事實上,Java中每個對象(除基本數(shù)據(jù)類型以外)的標識符都屬于指針的一種。但它們的使用受到了嚴格的限制和防范,不僅編譯器對它們有“戒心”,運行期系統(tǒng)也不例外。或者換從另一個角度說,Java有指針,但沒有傳統(tǒng)指針的麻煩。我曾一度將這種指針叫做“句柄”,但你可以把它想像成“安全指針”。和預(yù)備學(xué)校為學(xué)生提供的安全剪刀類似——除非特別有意,否則不會傷著自己,只不過有時要慢慢來,要習(xí)慣一些沉悶的工作。<br>
<br>
12.1 傳遞句柄<br>
將句柄傳遞進入一個方法時,指向的仍然是相同的對象。一個簡單的實驗可以證明這一點(若執(zhí)行這個程序時有麻煩,請參考第3章3.1.2小節(jié)“賦值”):<br>
<br>
542頁程序<br>
<br>
toString方法會在打印語句里自動調(diào)用,而PassHandles直接從Object繼承,沒有toString的重新定義。因此,這里會采用toString的Object版本,打印出對象的類,接著是那個對象所在的位置(不是句柄,而是對象的實際存儲位置)。輸出結(jié)果如下:<br>
p inside main(): PassHandles@1653748<br>
h inside f() : PassHandles@1653748<br>
可以看到,無論p還是h引用的都是同一個對象。這比復(fù)制一個新的PassHandles對象有效多了,使我們能將一個參數(shù)發(fā)給一個方法。但這樣做也帶來了另一個重要的問題。<br>
<br>
12.1.1 別名問題<br>
“別名”意味著多個句柄都試圖指向同一個對象,就象前面的例子展示的那樣。若有人向那個對象里寫入一點什么東西,就會產(chǎn)生別名問題。若其他句柄的所有者不希望那個對象改變,恐怕就要失望了。這可用下面這個簡單的例子說明:<br>
<br>
543頁程序<br>
<br>
對下面這行:<br>
Alias1 y = x; // Assign the handle<br>
它會新建一個Alias1句柄,但不是把它分配給由new創(chuàng)建的一個新鮮對象,而是分配給一個現(xiàn)有的句柄。所以句柄x的內(nèi)容——即對象x指向的地址——被分配給y,所以無論x還是y都與相同的對象連接起來。這樣一來,一旦x的i在下述語句中增值:<br>
x.i++;<br>
y的i值也必然受到影響。從最終的輸出就可以看出:<br>
<br>
544頁上程序<br>
<br>
此時最直接的一個解決辦法就是干脆不這樣做:不要有意將多個句柄指向同一個作用域內(nèi)的同一個對象。這樣做可使代碼更易理解和調(diào)試。然而,一旦準備將句柄作為一個自變量或參數(shù)傳遞——這是Java設(shè)想的正常方法——別名問題就會自動出現(xiàn),因為創(chuàng)建的本地句柄可能修改“外部對象”(在方法作用域之外創(chuàng)建的對象)。下面是一個例子:<br>
<br>
544頁程序<br>
<br>
輸出如下:<br>
x: 7<br>
Calling f(x)<br>
x: 8<br>
<br>
方法改變了自己的參數(shù)——外部對象。一旦遇到這種情況,必須判斷它是否合理,用戶是否愿意這樣,以及是不是會造成問題。<br>
通常,我們調(diào)用一個方法是為了產(chǎn)生返回值,或者用它改變?yōu)槠湔{(diào)用方法的那個對象的狀態(tài)(方法其實就是我們向那個對象“發(fā)一條消息”的方式)。很少需要調(diào)用一個方法來處理它的參數(shù);這叫作利用方法的“副作用”(Side
Effect)。所以倘若創(chuàng)建一個會修改自己參數(shù)的方法,必須向用戶明確地指出這一情況,并警告使用那個方法可能會有的后果以及它的潛在威脅。由于存在這些混淆和缺陷,所以應(yīng)該盡量避免改變參數(shù)。<br>
若需在一個方法調(diào)用期間修改一個參數(shù),且不打算修改外部參數(shù),就應(yīng)在自己的方法內(nèi)部制作一個副本,從而保護那個參數(shù)。本章的大多數(shù)內(nèi)容都是圍繞這個問題展開的。<br>
<br>
12.2 制作本地副本<br>
稍微總結(jié)一下:Java中的所有自變量或參數(shù)傳遞都是通過傳遞句柄進行的。也就是說,當我們傳遞“一個對象”時,實際傳遞的只是指向位于方法外部的那個對象的“一個句柄”。所以一旦要對那個句柄進行任何修改,便相當于修改外部對象。此外:<br>
■參數(shù)傳遞過程中會自動產(chǎn)生別名問題<br>
■不存在本地對象,只有本地句柄<br>
■句柄有自己的作用域,而對象沒有<br>
■對象的“存在時間”在Java里不是個問題<br>
■沒有語言上的支持(如常量)可防止對象被修改(以避免別名的副作用)<br>
若只是從對象中讀取信息,而不修改它,傳遞句柄便是自變量傳遞中最有效的一種形式。這種做非常恰當;默認的方法一般也是最有效的方法。然而,有時仍需將對象當作“本地的”對待,使我們作出的改變只影響一個本地副本,不會對外面的對象造成影響。許多程序設(shè)計語言都支持在方法內(nèi)自動生成外部對象的一個本地副本(注釋①)。盡管Java不具備這種能力,但允許我們達到同樣的效果。<br>
<br>
①:在C語言中,通常控制的是少量數(shù)據(jù)位,默認操作是按值傳遞。C++也必須遵照這一形式,但按值傳遞對象并非肯定是一種有效的方式。此外,在C++中用于支持按值傳遞的代碼也較難編寫,是件讓人頭痛的事情。<br>
<br>
12.2.1 按值傳遞<br>
首先要解決術(shù)語的問題,最適合“按值傳遞”的看起來是自變量。“按值傳遞”以及它的含義取決于如何理解程序的運行方式。最常見的意思是獲得要傳遞的任何東西的一個本地副本,但這里真正的問題是如何看待自己準備傳遞的東西。對于“按值傳遞”的含義,目前存在兩種存在明顯區(qū)別的見解:<br>
(1) Java按值傳遞任何東西。若將基本數(shù)據(jù)類型傳遞進入一個方法,會明確得到基本數(shù)據(jù)類型的一個副本。但若將一個句柄傳遞進入方法,得到的是句柄的副本。所以人們認為“一切”都按值傳遞。當然,這種說法也有一個前提:句柄肯定也會被傳遞。但Java的設(shè)計方案似乎有些超前,允許我們忽略(大多數(shù)時候)自己處理的是一個句柄。也就是說,它允許我們將句柄假想成“對象”,因為在發(fā)出方法調(diào)用時,系統(tǒng)會自動照管兩者間的差異。<br>
(2) Java主要按值傳遞(無自變量),但對象卻是按引用傳遞的。得到這個結(jié)論的前提是句柄只是對象的一個“別名”,所以不考慮傳遞句柄的問題,而是直接指出“我準備傳遞對象”。由于將其傳遞進入一個方法時沒有獲得對象的一個本地副本,所以對象顯然不是按值傳遞的。Sun公司似乎在某種程度上支持這一見解,因為它“保留但未實現(xiàn)”的關(guān)鍵字之一便是byvalue(按值)。但沒人知道那個關(guān)鍵字什么時候可以發(fā)揮作用。<br>
盡管存在兩種不同的見解,但其間的分歧歸根到底是由于對“句柄”的不同解釋造成的。我打算在本書剩下的部分里回避這個問題。大家不久就會知道,這個問題爭論下去其實是沒有意義的——最重要的是理解一個句柄的傳遞會使調(diào)用者的對象發(fā)生意外的改變。<br>
<br>
12.2.2 克隆對象<br>
若需修改一個對象,同時不想改變調(diào)用者的對象,就要制作該對象的一個本地副本。這也是本地副本最常見的一種用途。若決定制作一個本地副本,只需簡單地使用clone()方法即可。Clone是“克隆”的意思,即制作完全一模一樣的副本。這個方法在基礎(chǔ)類Object中定義成“protected”(受保護)模式。但在希望克隆的任何衍生類中,必須將其覆蓋為“public”模式。例如,標準庫類Vector覆蓋了clone(),所以能為Vector調(diào)用clone(),如下所示:<br>
<br>
547頁程序<br>
<br>
clone()方法產(chǎn)生了一個Object,后者必須立即重新造型為正確類型。這個例子指出Vector的clone()方法不能自動嘗試克隆Vector內(nèi)包含的每個對象——由于別名問題,老的Vector和克隆的Vector都包含了相同的對象。我們通常把這種情況叫作“簡單復(fù)制”或者“淺層復(fù)制”,因為它只復(fù)制了一個對象的“表面”部分。實際對象除包含這個“表面”以外,還包括句柄指向的所有對象,以及那些對象又指向的其他所有對象,由此類推。這便是“對象網(wǎng)”或“對象關(guān)系網(wǎng)”的由來。若能復(fù)制下所有這張網(wǎng),便叫作“全面復(fù)制”或者“深層復(fù)制”。<br>
在輸出中可看到淺層復(fù)制的結(jié)果,注意對v2采取的行動也會影響到v:<br>
<br>
548頁上程序<br>
<br>
一般來說,由于不敢保證Vector里包含的對象是“可以克隆”(注釋②)的,所以最好不要試圖克隆那些對象。<br>
<br>
②:“可以克隆”用英語講是cloneable,請留意Java庫中專門保留了這樣的一個關(guān)鍵字。<br>
<br>
12.2.3 使類具有克隆能力<br>
盡管克隆方法是在所有類最基本的Object中定義的,但克隆仍然不會在每個類里自動進行。這似乎有些不可思議,因為基礎(chǔ)類方法在衍生類里是肯定能用的。但Java確實有點兒反其道而行之;如果想在一個類里使用克隆方法,唯一的辦法就是專門添加一些代碼,以便保證克隆的正常進行。<br>
<br>
1. 使用protected時的技巧<br>
為避免我們創(chuàng)建的每個類都默認具有克隆能力,clone()方法在基礎(chǔ)類Object里得到了“保留”(設(shè)為protected)。這樣造成的后果就是:對那些簡單地使用一下這個類的客戶程序員來說,他們不會默認地擁有這個方法;其次,我們不能利用指向基礎(chǔ)類的一個句柄來調(diào)用clone()(盡管那樣做在某些情況下特別有用,比如用多形性的方式克隆一系列對象)。在編譯期的時候,這實際是通知我們對象不可克隆的一種方式——而且最奇怪的是,Java庫中的大多數(shù)類都不能克隆。因此,假如我們執(zhí)行下述代碼:<br>
Integer x = new Integer(l);<br>
x = x.clone();<br>
那么在編譯期,就有一條討厭的錯誤消息彈出,告訴我們不可訪問clone()——因為Integer并沒有覆蓋它,而且它對protected版本來說是默認的)。<br>
但是,假若我們是在一個從Object衍生出來的類中(所有類都是從Object衍生的),就有權(quán)調(diào)用Object.clone(),因為它是“protected”,而且我們在一個繼承器中。基礎(chǔ)類clone()提供了一個有用的功能——它進行的是對衍生類對象的真正“按位”復(fù)制,所以相當于標準的克隆行動。然而,我們隨后需要將自己的克隆操作設(shè)為public,否則無法訪問。總之,克隆時要注意的兩個關(guān)鍵問題是:幾乎肯定要調(diào)用super.clone(),以及注意將克隆設(shè)為public。<br>
有時還想在更深層的衍生類中覆蓋clone(),否則就直接使用我們的clone()(現(xiàn)在已成為public),而那并不一定是我們所希望的(然而,由于Object.clone()已制作了實際對象的一個副本,所以也有可能允許這種情況)。protected的技巧在這里只能用一次:首次從一個不具備克隆能力的類繼承,而且想使一個類變成“能夠克隆”。而在從我們的類繼承的任何場合,clone()方法都是可以使用的,因為Java不可能在衍生之后反而縮小方法的訪問范圍。換言之,一旦對象變得可以克隆,從它衍生的任何東西都是能夠克隆的,除非使用特殊的機制(后面討論)令其“關(guān)閉”克隆能力。<br>
<br>
2. 實現(xiàn)Cloneable接口<br>
為使一個對象的克隆能力功成圓滿,還需要做另一件事情:實現(xiàn)Cloneable接口。這個接口使人稍覺奇怪,因為它是空的!<br>
interface Cloneable {}<br>
之所以要實現(xiàn)這個空接口,顯然不是因為我們準備上溯造型成一個Cloneable,以及調(diào)用它的某個方法。有些人認為在這里使用接口屬于一種“欺騙”行為,因為它使用的特性打的是別的主意,而非原來的意思。Cloneable
interface的實現(xiàn)扮演了一個標記的角色,封裝到類的類型中。<br>
兩方面的原因促成了Cloneable interface的存在。首先,可能有一個上溯造型句柄指向一個基礎(chǔ)類型,而且不知道它是否真的能克隆那個對象。在這種情況下,可用instanceof關(guān)鍵字(第11章有介紹)調(diào)查句柄是否確實同一個能克隆的對象連接:<br>
?? 快捷鍵說明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -