44.java编程思想——传递和返回对象 制作本地副本
Java 中的所有自变量或参数传递都是通过传递句柄进行的。也就是说,当我们传递“一个对象”时,实际传递的只是指向位于方法外部的那个对象的“一个句柄”。所以一旦要对那个句柄进行任何修改,便相当于修改外部对象。此外:
■参数传递过程中会自动产生别名问题
■不存在本地对象,只有本地句柄
■句柄有自己的作用域,而对象没有
■对象的“存在时间”在Java 里不是个问题
■没有语言上的支持(如常量)可防止对象被修改(以避免别名的副作用)
若只是从对象中读取信息,而不修改它,传递句柄便是自变量传递中最有效的一种形式。这种做非常恰当;默认的方法一般也是最有效的方法。然而,有时仍需将对象当作“本地的”对待,使我们作出的改变只影响一个本地副本,不会对外面的对象造成影响。许多程序设计语言都支持在方法内自动生成外部对象的一个本地副本。尽管Java 不具备这种能力,但允许我们达到同样的效果。在C 语言中,通常控制的是少量数据位,默认操作是按值传递。C++也必须遵照这一形式,但按值传递对象并非肯定是一种有效的方式。此外,在C++中用于支持按值传递的代码也较难编写,是件让人头痛的事情。
1 按值传递
首先要解决术语的问题,最适合“按值传递”的看起来是自变量。“按值传递”以及它的含义取决于如何理解程序的运行方式。最常见的意思是获得要传递的任何东西的一个本地副本,但这里真正的问题是如何看待自己准备传递的东西。对于“按值传递”的含义,目前存在两种存在明显区别的见解:
(1) Java 按值传递任何东西。若将基本数据类型传递进入一个方法,会明确得到基本数据类型的一个副本。但若将一个句柄传递进入方法,得到的是句柄的副本。所以人们认为“一切”都按值传递。当然,这种说法也有一个前提:句柄肯定也会被传递。但Java 的设计方案似乎有些超前,允许我们忽略(大多数时候)自己处理的是一个句柄。也就是说,它允许我们将句柄假想成“对象”,因为在发出方法调用时,系统会自动照管两者间的差异。
(2) Java 主要按值传递(无自变量),但对象却是按引用传递的。得到这个结论的前提是句柄只是对象的一个“别名”,所以不考虑传递句柄的问题,而是直接指出“我准备传递对象”。由于将其传递进入一个方法时没有获得对象的一个本地副本,所以对象显然不是按值传递的。Sun 公司似乎在某种程度上支持这一见解,因为它“保留但未实现”的关键字之一便是byvalue(按值)。但没人知道那个关键字什么时候可以发挥作用。
尽管存在两种不同的见解,但其间的分歧归根到底是由于对“句柄”的不同解释造成的。我打算在本书剩下的部分里回避这个问题。大家不久就会知道,这个问题争论下去其实是没有意义的——最重要的是理解一个句柄的传递会使调用者的对象发生意外的改变。
2 克隆对象
若需修改一个对象,同时不想改变调用者的对象,就要制作该对象的一个本地副本。这也是本地副本最常见的一种用途。若决定制作一个本地副本,只需简单地使用clone()方法即可。Clone 是“克隆”的意思,即制作完全一模一样的副本。这个方法在基础类Object 中定义成“protected”(受保护)模式。但在希望克隆的任何衍生类中,必须将其覆盖为“public”模式。例如,标准库类Vector 覆盖了clone(),所以能为Vector 调用clone()。
2.1 代码
import java.util.*;
class Int {
privateinti;
public Int(intii){
i = ii;
}
publicvoidincrement() {
i++;
}
public String toString() {
return Integer.toString(i);
}
}
publicclass Cloning {
publicstaticvoidmain(String[]args){
Vector v = new Vector();
for (inti = 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 (Enumeratione = v2.elements();e.hasMoreElements();)
((Int) e.nextElement()).increment();
// See if it changed v's elements:
System.out.println("v: " +v);
}
} /// :~
2.2 执行
v:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
v:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
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 里包含的对象是“可以克隆”的,所以最好不要试图克隆那些对象。
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 关键字调查句柄是否确实同一个能克隆的对象连接:
if(myHandle instanceofCloneable) // ...
第二个原因是考虑到我们可能不愿所有对象类型都能克隆。所以Object.clone()会验证一个类是否真的是实现了Cloneable 接口。若答案是否定的,则“掷”出一个CloneNotSupportedException违例。所以在一般情况下,我们必须将“implement Cloneable”作为对克隆能力提供支持的一部分。
4 成功的克隆
importjava.util.*;
class MyObjectimplements Cloneable {
inti;
MyObject(intii) {
i = ii;
}
public Object clone() {
Object o =null;
try {
o =super.clone();
} catch (CloneNotSupportedExceptione) {
System.out.println("MyObject can't clone");
}
returno;
}
public String toString() {
return Integer.toString(i);
}
}
publicclass LocalCopy {
static MyObject g(MyObjectv) {
// Passing a handle, modifies outside object:
v.i++;
returnv;
}
static MyObject f(MyObjectv) {
v = (MyObject)v.clone(); // Local copy
v.i++;
returnv;
}
publicstaticvoidmain(String[]args){
MyObject a =new MyObject(11);
MyObject b =g(a);
// Testing handle equivalence,
// not object equivalence:
if (a ==b)
System.out.println("a == b");
else
System.out.println("a != b");
System.out.println("a = " +a);
System.out.println("b = " +b);
MyObject c =new MyObject(47);
MyObject d =f(c);
if (c ==d)
System.out.println("c == d");
else
System.out.println("c != d");
System.out.println("c = " +c);
System.out.println("d = " +d);
}
} /// :~
5 执行
不管怎样,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()中,两种不同参数传递方式的区别在于它们分别测试了一个不同的方法。
Java 对“是否等价”的测试并不对所比较对象的内部进行检查,从而核实它们的值是否相同。==和!=运算符只是简单地对比句柄的内容。若句柄内的地址相同,就认为句柄指向同样的对象,所以认为它们是“等价”的。所以运算符真正检测的是“由于别名问题,句柄是否指向同一个对象?”
6 O b j e c t . c l o n e ( )的效果
调用Object.clone()时,实际发生的是什么事情呢?当我们在自己的类里覆盖clone()时,什么东西对于super.clone()来说是最关键的呢?根类中的clone()方法负责建立正确的存储容量,并通过“按位复制”将二进制位从原始对象中复制到新对象的存储空间。也就是说,它并不只是预留存储空间以及复制一个对象——实际需要调查出欲复制之对象的准确大小,然后复制那个对象。由于所有这些工作都是在由根类定义之clone()方法的内部代码中进行的(根类并不知道要从自己这里继承出去什么),所以大家或许已经猜到,这个过程需要用RTTI 判断欲克隆的对象的实际大小。采取这种方式,clone()方法便可建立起正确数量的存储空间,并对那个类型进行正确的按位复制。
不管我们要做什么,克隆过程的第一个部分通常都应该是调用super.clone()。通过进行一次准确的复制,这样做可为后续的克隆进程建立起一个良好的基础。随后,可采取另一些必要的操作,以完成最终的克隆。
为确切了解其他操作是什么,首先要正确理解Object.clone()为我们带来了什么。特别地,它会自动克隆所有句柄指向的目标吗?
6.1 代码
publicclass Snake implements Cloneable {
private Snakenext;
privatecharc;
// Value of i== number of segments
Snake(inti, charx) {
c = x;
if (--i > 0)
next =new Snake(i, (char) (x + 1));
}
void increment() {
c++;
if (next !=null)
next.increment();
}
public String toString() {
String s =":" + c;
if (next !=null)
s +=next.toString();
returns;
}
public Object clone() {
Object o =null;
try {
o =super.clone();
} catch (CloneNotSupportedExceptione) {
}
returno;
}
publicstaticvoidmain(String[]args){
Snake s =new Snake(5, 'a');
System.out.println("s = " +s);
Snake s2 = (Snake)s.clone();
System.out.println("s2 = " +s2);
s.increment();
System.out.println("after s.increment, s2 = "+s2);
}
} /// :~
6.2 执行
s= :a:b:c:d:e
s2= :a:b:c:d:e
afters.increment, s2 = :a:c:d:e:f
一条Snake(蛇)由数段构成,每一段的类型都是Snake。所以,这是一个一段段链接起来的列表。所有段都是以循环方式创建的,每做好一段,都会使第一个构建器参数的值递减,直至最终为零。而为给每段赋予一个独一无二的标记,第二个参数(一个Char)的值在每次循环构建器调用时都会递增。increment()方法的作用是循环递增每个标记,使我们能看到发生的变化;而toString 则循环打印出每个标记。
这意味着只有第一段才是由Object.clone()复制的,所以此时进行的是一种“浅层复制”。若希望复制整条蛇——即进行“深层复制”——必须在被覆盖的clone()里采取附加的操作。
通常可在从一个能克隆的类里调用super.clone(),以确保所有基础类行动(包括Object.clone())能够进行。随着是为对象内每个句柄都明确调用一个clone();否则那些句柄会别名变成原始对象的句柄。构建器的调用也大致相同——首先构造基础类,然后是下一个衍生的构建器⋯⋯以此类推,直到位于最深层的衍生构建器。区别在于clone()并不是个构建器,所以没有办法实现自动克隆。为了克隆,必须由自己明确进行。
7 克隆合成对象
试图深层复制合成对象时会遇到一个问题。必须假定成员对象中的clone()方法也能依次对自己的句柄进行深层复制,以此类推。这使我们的操作变得复杂。为了能正常实现深层复制,必须对所有类中的代码进行控制,或者至少全面掌握深层复制中需要涉及的类,确保它们自己的深层复制能正确进行。
7.1 代码
class DepthReadingimplements Cloneable {
privatedoubledepth;
public DepthReading(doubledepth){
this.depth =depth;
}
public Object clone() {
Object o =null;
try {
o =super.clone();
} catch (CloneNotSupportedExceptione) {
e.printStackTrace();
}
returno;
}
}
class TemperatureReadingimplements Cloneable {
privatelongtime;
privatedoubletemperature;
public TemperatureReading(doubletemperature){
time = System.currentTimeMillis();
this.temperature =temperature;
}
public Object clone() {
Object o =null;
try {
o =super.clone();
} catch (CloneNotSupportedExceptione) {
e.printStackTrace();
}
returno;
}
}
class OceanReadingimplements Cloneable {
private DepthReadingdepth;
private TemperatureReadingtemperature;
public OceanReading(doubletdata,doubleddata){
temperature =new TemperatureReading(tdata);
depth =new DepthReading(ddata);
}
public Object clone() {
OceanReading o = null;
try {
o = (OceanReading)super.clone();
} catch (CloneNotSupportedExceptione) {
e.printStackTrace();
}
// Must clone handles:
o.depth = (DepthReading)o.depth.clone();
o.temperature = (TemperatureReading)o.temperature.clone();
returno; // Upcastsback to Object
}
}
publicclass DeepCopy {
publicstaticvoidmain(String[]args){
OceanReading reading = new OceanReading(33.9, 100.5);
// Now clone it:
OceanReading r = (OceanReading)reading.clone();
}
} /// :~
7.2 执行
DepthReading 和TemperatureReading 非常相似;它们都只包含了基本数据类型。所以clone()方法能够非常简单:调用super.clone()并返回结果即可。注意两个类使用的clone()代码是完全一致的。OceanReading 是由DepthReading 和TemperatureReading 对象合并而成的。为了对其进行深层复制,clone()必须同时克隆OceanReading 内的句柄。为达到这个目标,super.clone()的结果必须造型成一个OceanReading 对象(以便访问depth 和temperature 句柄)。
8 用V e c t o r 进行深层复制
8.1 代码
import java.util.*;
class Int2implements Cloneable {
privateinti;
public Int2(intii){
i = ii;
}
publicvoidincrement() {
i++;
}
public String toString() {
return Integer.toString(i);
}
public Object clone() {
Object o =null;
try {
o =super.clone();
} catch (CloneNotSupportedExceptione) {
System.out.println("Int2 can't clone");
}
returno;
}
}
class Int3extends Int2 {
privateintj;// Automatically duplicated
public Int3(inti) {
super(i);
}
}
publicclass AddingClone {
publicstaticvoidmain(String[]args){
Int2 x =new Int2(10);
Int2 x2 = (Int2)x.clone();
x2.increment();
System.out.println("x = " +x + ", x2 = "+x2);