# 12.2 制作本地副本
稍微总结一下:Java中的所有参数传递都是通过传递引用进行的。也就是说,当我们传递“一个对象”时,实际传递的只是指向位于方法外部的那个对象的“一个引用”。所以一旦要对那个引用进行任何修改,便相当于修改外部对象。此外:
+ 参数传递过程中会自动产生别名问题
+ 不存在本地对象,只有本地引用
+ 引用有自己的作用域,而对象没有
+ 对象的“存在时间”在Java里不是个问题
+ 没有语言上的支持(如常量)可防止对象被修改(以避免别名的副作用)
若只是从对象中读取信息,而不修改它,传递引用便是参数传递中最有效的一种形式。这种做非常恰当;默认的方法一般也是最有效的方法。然而,有时仍需将对象当作“本地的”对待,使我们作出的改变只影响一个本地副本,不会对外面的对象造成影响。许多程序设计语言都支持在方法内自动生成外部对象的一个本地副本(注释①)。尽管Java不具备这种能力,但允许我们达到同样的效果。
①:在C语言中,通常控制的是少量数据位,默认操作是按值传递。C++也必须遵照这一形式,但按值传递对象并非肯定是一种有效的方式。此外,在C++中用于支持按值传递的代码也较难编写,是件让人头痛的事情。
## 12.2.1 按值传递
首先要解决术语的问题,最适合“按值传递”的看起来是参数。“按值传递”以及它的含义取决于如何理解程序的运行方式。最常见的意思是获得要传递的任何东西的一个本地副本,但这里真正的问题是如何看待自己准备传递的东西。对于“按值传递”的含义,目前存在两种存在明显区别的见解:
(1) Java按值传递任何东西。若将基本数据类型传递进入一个方法,会明确得到基本数据类型的一个副本。但若将一个引用传递进入方法,得到的是引用的副本。所以人们认为“一切”都按值传递。当然,这种说法也有一个前提:引用肯定也会被传递。但Java的设计模式似乎有些超前,允许我们忽略(大多数时候)自己处理的是一个引用。也就是说,它允许我们将引用假想成“对象”,因为在发出方法调用时,系统会自动照管两者间的差异。
(2) Java主要按值传递(无参数),但对象却是按引用传递的。得到这个结论的前提是引用只是对象的一个“别名”,所以不考虑传递引用的问题,而是直接指出“我准备传递对象”。由于将其传递进入一个方法时没有获得对象的一个本地副本,所以对象显然不是按值传递的。Sun公司似乎在某种程度上支持这一见解,因为它“保留但未实现”的关键字之一便是`byvalue`(按值)。但没人知道那个关键字什么时候可以发挥作用。
尽管存在两种不同的见解,但其间的分歧归根到底是由于对“引用”的不同解释造成的。我打算在本书剩下的部分里回避这个问题。大家不久就会知道,这个问题争论下去其实是没有意义的——最重要的是理解一个引用的传递会使调用者的对象发生意外的改变。
## 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库中专门保留了这样的一个关键字。
## 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不可能在派生之后反而缩小方法的访问范围。换言之,一旦对象变得可以克隆,从它派生的任何东西都是能够克隆的,除非使用特殊的机制(后面讨论)令其“关闭”克隆能力。
(2) 实现`Cloneable`接口
为使一个对象的克隆能力功成圆满,还需要做另一件事情:实现`Cloneable`接口。这个接口使人稍觉奇怪,因为它是空的!
```
interface Cloneable {}
```
之所以要实现这个空接口,显然不是因为我们准备向上转换成一个`Cloneable`,以及调用它的某个方法。有些人认为在这里使用接口属于一种“欺骗”行为,因为它使用的特性打的是别的主意,而非原来的意思。`Cloneable interface`的实现扮演了一个标记的角色,封装到类的类型中。
两方面的原因促成了`Cloneable interface`的存在。首先,可能有一个向上转换引用指向一个基类型,而且不知道它是否真的能克隆那个对象。在这种情况下,可用`instanceof`关键字(第11章有介绍)调查引用是否确实同一个能克隆的对象连接:
```
if(myHandle instanceof Cloneable) // ...
```
第二个原因是考虑到我们可能不愿所有对象类型都能克隆。所以`Object.clone()`会验证一个类是否真的是实现了`Cloneable`接口。若答案是否定的,则“抛”出一个`CloneNotSupportedException`异常。所以在一般情况下,我们必须将`implement Cloneable`作为对克隆能力提供支持的一部分。
## 12.2.4 成功的克隆
理解了实现`clone()`方法背后的所有细节后,便可创建出能方便复制的类,以便提供了一个本地副本:
```
//: LocalCopy.java
// Creating local copies with clone()
import java.util.*;
class MyObject implements Cloneable {
int i;
MyObject(int ii) { i = ii; }
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
System.out.println("MyObject can't clone");
}
return o;
}
public String toString() {
return Integer.toString(i);
}
}
public class LocalCopy {
static MyObject g(MyObject v) {
// Passing a handle, modifies outside object:
v.i++;
return v;
}
static MyObject f(MyObject v) {
v = (MyObject)v.clone(); // Local copy
v.i++;
return v;
}
public static void main(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);
}
} ///:~
```
不管怎样,`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()`中,两种不同参数传递方式的区别在于它们分别测试了一个不同的方法。输出结果如下:
```
a == b
a = 12
b = 12
c != d
c = 47
d = 48
```
大家要记住这样一个事实:Java对“是否等价”的测试并不对所比较对象的内部进行检查,从而核实它们的值是否相同。`==`和`!=`运算符只是简单地对比引用的内容。若引用内的地址相同,就认为引用指向同样的对象,所以认为它们是“等价”的。所以运算符真正检测的是“由于别名问题,引用是否指向同一个对象?”
## 12.2.5 `Object.clone()`的效果
调用`Object.clone()`时,实际发生的是什么事情呢?当我们在自己的类里覆盖`clone()`时,什么东西对于`super.clone()`来说是最关键的呢?根类中的`clone()`方法负责建立正确的存储容量,并通过“按位复制”将二进制位从原始对象中复制到新对象的存储空间。也就是说,它并不只是预留存储空间以及复制一个对象——实际需要调查出欲复制之对象的准确大小,然后复制那个对象。由于所有这些工作都是在由根类定义之`clone()`方法的内部代码中进行的(根类并不知道要从自己这里继承出去什么),所以大家或许已经猜到,这个过程需要用RTTI判断欲克隆的对象的实际大小。采取这种方式,`clone()`方法便可建立起正确数量的存储空间,并对那个类型进行正确的按位复制。
不管我们要做什么,克隆过程的第一个部分通常都应该是调用`super.clone()`。通过进行一次准确的复制,这样做可为后续的克隆进程建立起一个良好的基础。随后,可采取另一些必要的操作,以完成最终的克隆。
为确切了解其他操作是什么,首先要正确理解`Object.clone()`为我们带来了什么。特别地,它会自动克隆所有引用指向的目标吗?下面这个例子可完成这种形式的检测:
```
//: Snake.java
// Tests cloning to see if destination of
// handles are also cloned.
public class Snake implements Cloneable {
private Snake next;
private char c;
// Value of i == number of segments
Snake(int i, char x) {
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();
return s;
}
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {}
return o;
}
public static void main(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);
}
} ///:~
```
一条`Snake`(蛇)由数段构成,每一段的类型都是`Snake`。所以,这是一个一段段链接起来的列表。所有段都是以循环方式创建的,每做好一段,都会使第一个构造器参数的值递减,直至最终为零。而为给每段赋予一个独一无二的标记,第二个参数(一个`Char`)的值在每次循环构造器调用时都会递增。
`increment()`方法的作用是循环递增每个标记,使我们能看到发生的变化;而`toString`则循环打印出每个标记。输出如下:
```
s = :a:b:c:d:e
s2 = :a:b:c:d:e
after s.increment, s2 = :a:c:d:e:f
```
这意味着只有第一段才是由`Object.clone()`复制的,所以此时进行的是一种“浅层复制”。若希望复制整条蛇——即进行“深层复制”——必须在被覆盖的`clone()`里采取附加的操作。
通常可在从一个能克隆的类里调用`super.clone()`,以确保所有基类行动(包括`Object.clone()`)能够进行。随着是为对象内每个引用都明确调用一个`clone()`;否则那些引用会别名变成原始对象的引用。构造器的调用也大致相同——首先构造基类,然后是下一个派生的构造器……以此类推,直到位于最深层的派生构造器。区别在于`clone()`并不是个构造器,所以没有办法实现自动克隆。为了克隆,必须由自己明确进行。
## 12.2.6 克隆组合对象
试图深层复制组合对象时会遇到一个问题。必须假定成员对象中的`clone()`方法也能依次对自己的引用进行深层复制,以此类推。这使我们的操作变得复杂。为了能正常实现深层复制,必须对所有类中的代码进行控制,或者至少全面掌握深层复制中需要涉及的类,确保它们自己的深层复制能正确进行。
下面这个例子总结了面对一个组合对象进行深层复制时需要做哪些事情:
```
//: DeepCopy.java
// Cloning a composed object
class DepthReading implements Cloneable {
private double depth;
public DepthReading(double depth) {
this.depth = depth;
}
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
}
class TemperatureReading implements Cloneable {
private long time;
private double temperature;
public TemperatureReading(double temperature) {
time = System.currentTimeMillis();
this.temperature = temperature;
}
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
}
class OceanReading implements Cloneable {
private DepthReading depth;
private TemperatureReading temperature;
public OceanReading(double tdata, double ddata){
temperature = new TemperatureReading(tdata);
depth = new DepthReading(ddata);
}
public Object clone() {
OceanReading o = null;
try {
o = (OceanReading)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
// Must clone handles:
o.depth = (DepthReading)o.depth.clone();
o.temperature =
(TemperatureReading)o.temperature.clone();
return o; // Upcasts back to Object
}
}
public class DeepCopy {
public static void main(String[] args) {
OceanReading reading =
new OceanReading(33.9, 100.5);
// Now clone it:
OceanReading r =
(OceanReading)reading.clone();
}
} ///:~
```
`DepthReading`和`TemperatureReading`非常相似;它们都只包含了基本数据类型。所以`clone()`方法能够非常简单:调用`super.clone()`并返回结果即可。注意两个类使用的`clone()`代码是完全一致的。
`OceanReading`是由`DepthReading`和`TemperatureReading`对象合并而成的。为了对其进行深层复制,`clone()`必须同时克隆`OceanReading`内的引用。为达到这个目标,`super.clone()`的结果必须转换成一个`OceanReading`对象(以便访问`depth`和`temperature`引用)。
## 12.2.7 用`Vector`进行深层复制
下面让我们复习一下本章早些时候提出的`Vector`例子。这一次`Int2`类是可以克隆的,所以能对`Vector`进行深层复制:
```
//: AddingClone.java
// You must go through a few gyrations to
// add cloning to your own class.
import java.util.*;
class Int2 implements Cloneable {
private int i;
public Int2(int ii) { i = ii; }
public void increment() { i++; }
public String toString() {
return Integer.toString(i);
}
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
System.out.println("Int2 can't clone");
}
return o;
}
}
// Once it's cloneable, inheritance
// doesn't remove cloneability:
class Int3 extends Int2 {
private int j; // Automatically duplicated
public Int3(int i) { super(i); }
}
public class AddingClone {
public static void main(String[] args) {
Int2 x = new Int2(10);
Int2 x2 = (Int2)x.clone();
x2.increment();
System.out.println(
"x = " + x + ", x2 = " + x2);
// Anything inherited is also cloneable:
Int3 x3 = new Int3(7);
x3 = (Int3)x3.clone();
Vector v = new Vector();
for(int i = 0; i < 10; i++ )
v.addElement(new Int2(i));
System.out.println("v: " + v);
Vector v2 = (Vector)v.clone();
// Now clone each element:
for(int i = 0; i < v.size(); i++)
v2.setElementAt(
((Int2)v2.elementAt(i)).clone(), i);
// Increment all v2's elements:
for(Enumeration e = v2.elements();
e.hasMoreElements(); )
((Int2)e.nextElement()).increment();
// See if it changed v's elements:
System.out.println("v: " + v);
System.out.println("v2: " + v2);
}
} ///:~
```
`Int3`自`Int2`继承而来,并添加了一个新的基本类型成员`int j`。大家也许认为自己需要再次覆盖`clone()`,以确保`j`得到复制,但实情并非如此。将`Int2`的`clone()`当作`Int3`的`clone()`调用时,它会调用`Object.clone()`,判断出当前操作的是`Int3`,并复制`Int3`内的所有二进制位。只要没有新增需要克隆的引用,对`Object.clone()`的一个调用就能完成所有必要的复制——无论`clone()`是在层次结构多深的一级定义的。
至此,大家可以总结出对`Vector`进行深层复制的先决条件:在克隆了`Vector`后,必须在其中遍历,并克隆由`Vector`指向的每个对象。为了对`Hashtable`(散列表)进行深层复制,也必须采取类似的处理。
这个例子剩余的部分显示出克隆已实际进行——证据就是在克隆了对象以后,可以自由改变它,而原来那个对象不受任何影响。
## 12.2.8 通过序列化进行深层复制
若研究一下第10章介绍的那个Java 1.1对象序列化示例,可能发现若在一个对象序列化以后再撤消对它的序列化,或者说进行装配,那么实际经历的正是一个“克隆”的过程。
那么为什么不用序列化进行深层复制呢?下面这个例子通过计算执行时间对比了这两种方法:
```
//: Compete.java
import java.io.*;
class Thing1 implements Serializable {}
class Thing2 implements Serializable {
Thing1 o1 = new Thing1();
}
class Thing3 implements Cloneable {
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
System.out.println("Thing3 can't clone");
}
return o;
}
}
class Thing4 implements Cloneable {
Thing3 o3 = new Thing3();
public Object clone() {
Thing4 o = null;
try {
o = (Thing4)super.clone();
} catch (CloneNotSupportedException e) {
System.out.println("Thing4 can't clone");
}
// Clone the field, too:
o.o3 = (Thing3)o3.clone();
return o;
}
}
public class Compete {
static final int SIZE = 5000;
public static void main(String[] args) {
Thing2[] a = new Thing2[SIZE];
for(int i = 0; i < a.length; i++)
a[i] = new Thing2();
Thing4[] b = new Thing4[SIZE];
for(int i = 0; i < b.length; i++)
b[i] = new Thing4();
try {
long t1 = System.currentTimeMillis();
ByteArrayOutputStream buf =
new ByteArrayOutputStream();
ObjectOutputStream o =
new ObjectOutputStream(buf);
for(int i = 0; i < a.length; i++)
o.writeObject(a[i]);
// Now get copies:
ObjectInputStream in =
new ObjectInputStream(
new ByteArrayInputStream(
buf.toByteArray()));
Thing2[] c = new Thing2[SIZE];
for(int i = 0; i < c.length; i++)
c[i] = (Thing2)in.readObject();
long t2 = System.currentTimeMillis();
System.out.println(
"Duplication via serialization: " +
(t2 - t1) + " Milliseconds");
// Now try cloning:
t1 = System.currentTimeMillis();
Thing4[] d = new Thing4[SIZE];
for(int i = 0; i < d.length; i++)
d[i] = (Thing4)b[i].clone();
t2 = System.currentTimeMillis();
System.out.println(
"Duplication via cloning: " +
(t2 - t1) + " Milliseconds");
} catch(Exception e) {
e.printStackTrace();
}
}
} ///:~
```
其中,`Thing2`和`Thing4`包含了成员对象,所以需要进行一些深层复制。一个有趣的地方是尽管`Serializable`类很容易设置,但在复制它们时却要做多得多的工作。克隆涉及到大量的类设置工作,但实际的对象复制是相当简单的。结果很好地说明了一切。下面是几次运行分别得到的结果:
```
Duplication via serialization: 3400 Milliseconds
Duplication via cloning: 110 Milliseconds
Duplication via serialization: 3410 Milliseconds
Duplication via cloning: 110 Milliseconds
Duplication via serialization: 3520 Milliseconds
Duplication via cloning: 110 Milliseconds
```
除了序列化和克隆之间巨大的时间差异以外,我们也注意到序列化技术的运行结果并不稳定,而克隆每一次花费的时间都是相同的。
## 12.2.9 使克隆具有更大的深度
若新建一个类,它的基类会默认为`Object`,并默认为不具备克隆能力(就象在下一节会看到的那样)。只要不明确地添加克隆能力,这种能力便不会自动产生。但我们可以在任何层添加它,然后便可从那个层开始向下具有克隆能力。如下所示:
```
//: HorrorFlick.java
// You can insert Cloneability at any
// level of inheritance.
import java.util.*;
class Person {}
class Hero extends Person {}
class Scientist extends Person
implements Cloneable {
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
// this should never happen:
// It's Cloneable already!
throw new InternalError();
}
}
}
class MadScientist extends Scientist {}
public class HorrorFlick {
public static void main(String[] args) {
Person p = new Person();
Hero h = new Hero();
Scientist s = new Scientist();
MadScientist m = new MadScientist();
// p = (Person)p.clone(); // Compile error
// h = (Hero)h.clone(); // Compile error
s = (Scientist)s.clone();
m = (MadScientist)m.clone();
}
} ///:~
```
添加克隆能力之前,编译器会阻止我们的克隆尝试。一旦在`Scientist`里添加了克隆能力,那么`Scientist`以及它的所有“后裔”都可以克隆。
## 12.2.10 为什么有这个奇怪的设计
之所以感觉这个方案的奇特,因为它事实上的确如此。也许大家会奇怪它为什么要象这样运行,而该方案背后的真正含义是什么呢?后面讲述的是一个未获证实的故事——大概是由于围绕Java的许多买卖使其成为一种设计优良的语言——但确实要花许多口舌才能讲清楚这背后发生的所有事情。
最初,Java只是作为一种用于控制硬件的语言而设计,与因特网并没有丝毫联系。象这样一类面向大众的语言一样,其意义在于程序员可以对任意一个对象进行克隆。这样一来,`clone()`就放置在根类`Object`里面,但因为它是一种公用方式,因而我们通常能够对任意一个对象进行克隆。看来这是最灵活的方式了,毕竟它不会带来任何害处。
正当Java看起来象一种终级因特网程序设计语言的时候,情况却发生了变化。突然地,人们提出了安全问题,而且理所当然,这些问题与使用对象有关,我们不愿望任何人克隆自己的保密对象。所以我们最后看到的是为原来那个简单、直观的方案添加的大量补丁:`clone()`在`Object`里被设置成`protected`。必须将其覆盖,并使用`implement Cloneable`,同时解决异常的问题。
只有在准备调用`Object`的`clone()`方法时,才没有必要使用`Cloneable`接口,因为那个方法会在运行期间得到检查,以确保我们的类实现了`Cloneable`。但为了保持连贯性(而且由于`Cloneable`无论如何都是空的),最好还是由自己实现`Cloneable`。
- Java 编程思想
- 写在前面的话
- 引言
- 第1章 对象入门
- 1.1 抽象的进步
- 1.2 对象的接口
- 1.3 实现方案的隐藏
- 1.4 方案的重复使用
- 1.5 继承:重新使用接口
- 1.6 多态对象的互换使用
- 1.7 对象的创建和存在时间
- 1.8 异常控制:解决错误
- 1.9 多线程
- 1.10 永久性
- 1.11 Java和因特网
- 1.12 分析和设计
- 1.13 Java还是C++
- 第2章 一切都是对象
- 2.1 用引用操纵对象
- 2.2 所有对象都必须创建
- 2.3 绝对不要清除对象
- 2.4 新建数据类型:类
- 2.5 方法、参数和返回值
- 2.6 构建Java程序
- 2.7 我们的第一个Java程序
- 2.8 注释和嵌入文档
- 2.9 编码样式
- 2.10 总结
- 2.11 练习
- 第3章 控制程序流程
- 3.1 使用Java运算符
- 3.2 执行控制
- 3.3 总结
- 3.4 练习
- 第4章 初始化和清除
- 4.1 用构造器自动初始化
- 4.2 方法重载
- 4.3 清除:收尾和垃圾收集
- 4.4 成员初始化
- 4.5 数组初始化
- 4.6 总结
- 4.7 练习
- 第5章 隐藏实现过程
- 5.1 包:库单元
- 5.2 Java访问指示符
- 5.3 接口与实现
- 5.4 类访问
- 5.5 总结
- 5.6 练习
- 第6章 类复用
- 6.1 组合的语法
- 6.2 继承的语法
- 6.3 组合与继承的结合
- 6.4 到底选择组合还是继承
- 6.5 protected
- 6.6 累积开发
- 6.7 向上转换
- 6.8 final关键字
- 6.9 初始化和类装载
- 6.10 总结
- 6.11 练习
- 第7章 多态性
- 7.1 向上转换
- 7.2 深入理解
- 7.3 覆盖与重载
- 7.4 抽象类和方法
- 7.5 接口
- 7.6 内部类
- 7.7 构造器和多态性
- 7.8 通过继承进行设计
- 7.9 总结
- 7.10 练习
- 第8章 对象的容纳
- 8.1 数组
- 8.2 集合
- 8.3 枚举器(迭代器)
- 8.4 集合的类型
- 8.5 排序
- 8.6 通用集合库
- 8.7 新集合
- 8.8 总结
- 8.9 练习
- 第9章 异常差错控制
- 9.1 基本异常
- 9.2 异常的捕获
- 9.3 标准Java异常
- 9.4 创建自己的异常
- 9.5 异常的限制
- 9.6 用finally清除
- 9.7 构造器
- 9.8 异常匹配
- 9.9 总结
- 9.10 练习
- 第10章 Java IO系统
- 10.1 输入和输出
- 10.2 增添属性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File类
- 10.5 IO流的典型应用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 压缩
- 10.9 对象序列化
- 10.10 总结
- 10.11 练习
- 第11章 运行期类型识别
- 11.1 对RTTI的需要
- 11.2 RTTI语法
- 11.3 反射:运行期类信息
- 11.4 总结
- 11.5 练习
- 第12章 传递和返回对象
- 12.1 传递引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只读类
- 12.5 总结
- 12.6 练习
- 第13章 创建窗口和程序片
- 13.1 为何要用AWT?
- 13.2 基本程序片
- 13.3 制作按钮
- 13.4 捕获事件
- 13.5 文本字段
- 13.6 文本区域
- 13.7 标签
- 13.8 复选框
- 13.9 单选钮
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 视窗化应用
- 13.16 新型AWT
- 13.17 Java 1.1用户接口API
- 13.18 可视编程和Beans
- 13.19 Swing入门
- 13.20 总结
- 13.21 练习
- 第14章 多线程
- 14.1 反应灵敏的用户界面
- 14.2 共享有限的资源
- 14.3 堵塞
- 14.4 优先级
- 14.5 回顾runnable
- 14.6 总结
- 14.7 练习
- 第15章 网络编程
- 15.1 机器的标识
- 15.2 套接字
- 15.3 服务多个客户
- 15.4 数据报
- 15.5 一个Web应用
- 15.6 Java与CGI的沟通
- 15.7 用JDBC连接数据库
- 15.8 远程方法
- 15.9 总结
- 15.10 练习
- 第16章 设计模式
- 16.1 模式的概念
- 16.2 观察器模式
- 16.3 模拟垃圾回收站
- 16.4 改进设计
- 16.5 抽象的应用
- 16.6 多重分发
- 16.7 访问器模式
- 16.8 RTTI真的有害吗
- 16.9 总结
- 16.10 练习
- 第17章 项目
- 17.1 文字处理
- 17.2 方法查找工具
- 17.3 复杂性理论
- 17.4 总结
- 17.5 练习
- 附录A 使用非JAVA代码
- 附录B 对比C++和Java
- 附录C Java编程规则
- 附录D 性能
- 附录E 关于垃圾收集的一些话
- 附录F 推荐读物