Java编程思想第4版[中文版](PDF格式)-第94部分
按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!
因此,这里会采用toString 的Object 版本,打印出对象的类,接着是那个对象所在的位置(不是句柄,而
是对象的实际存储位置)。输出结果如下:
p inside main(): PassHandles@1653748
h inside f() : PassHandles@1653748
可以看到,无论p 还是h 引用的都是同一个对象。这比复制一个新的PassHandles 对象有效多了,使我们能
将一个参数发给一个方法。但这样做也带来了另一个重要的问题。
12。1。1 别名问题
“别名”意味着多个句柄都试图指向同一个对象,就象前面的例子展示的那样。若有人向那个对象里写入一
点什么东西,就会产生别名问题。若其他句柄的所有者不希望那个对象改变,恐怕就要失望了。这可用下面
这个简单的例子说明:
//: Alias1。java
// Aliasing two handles to one object
349
…………………………………………………………Page 351……………………………………………………………
public class Alias1 {
int i;
Alias1(int ii) { i = ii; }
public static void main(String'' args) {
Alias1 x = new Alias1(7);
Alias1 y = x; // Assign the handle
System。out。println(〃x: 〃 + x。i);
System。out。println(〃y: 〃 + y。i);
System。out。println(〃Incrementing x〃);
x。i++;
System。out。println(〃x: 〃 + x。i);
System。out。println(〃y: 〃 + 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。java
// Method calls implicitly alias their
// arguments。
public class Alias2 {
int i;
Alias2(int ii) { i = ii; }
static void f(Alias2 handle) {
handle。i++;
}
public static void main(String'' args) {
Alias2 x = new Alias2(7);
System。out。println(〃x: 〃 + x。i);
System。out。println(〃Calling f(x)〃);
f(x);
System。out。println(〃x: 〃 + x。i);
}
} ///:~
350
…………………………………………………………Page 352……………………………………………………………
输出如下:
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 (按值)。但没人知道那个关键字什么时候可以发
挥作用。
尽管存在两种不同的见解,但其间的分歧归根到底是由于对“句柄”的不同解释造成的。我打算在本书剩下
的部分里回避这个问题。大家不久就会知道,这个问题争论下去其实是没有意义的——最重要的是理解一个
句柄的传递会使调用者的对象发生意外的改变。
351
…………………………………………………………Page 353……………………………………………………………
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 库中专门保留了这样的一个关键字。
352
…………………………………………………………Page 354……………………………………………………………
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 不可能