# 12.3 克隆的控制
为消除克隆能力,大家也许认为只需将`clone()`方法简单地设为`private`(私有)即可,但这样是行不通的,因为不能采用一个基类方法,并使其在派生类中更“私有”。所以事情并没有这么简单。此外,我们有必要控制一个对象是否能够克隆。对于我们设计的一个类,实际有许多种方案都是可以采取的:
(1) 保持中立,不为克隆做任何事情。也就是说,尽管不可对我们的类克隆,但从它继承的一个类却可根据实际情况决定克隆。只有`Object.clone()`要对类中的字段进行某些合理的操作时,才可以作这方面的决定。
(2) 支持`clone()`,采用实现`Cloneable`(可克隆)能力的标准操作,并覆盖`clone()`。在被覆盖的`clone()`中,可调用`super.clone()`,并捕获所有异常(这样可使`clone()`不“抛”出任何异常)。
(3) 有条件地支持克隆。若类容纳了其他对象的引用,而那些对象也许能够克隆(集合类便是这样的一个例子),就可试着克隆拥有对方引用的所有对象;如果它们“抛”出了异常,只需让这些异常通过即可。举个例子来说,假设有一个特殊的`Vector`,它试图克隆自己容纳的所有对象。编写这样的一个`Vector`时,并不知道客户程序员会把什么形式的对象置入这个`Vector`中,所以并不知道它们是否真的能够克隆。
(4) 不实现`Cloneable()`,但是将`clone()`覆盖成`protected`,使任何字段都具有正确的复制行为。这样一来,从这个类继承的所有东西都能覆盖`clone()`,并调用`super.clone()`来产生正确的复制行为。注意在我们实现方案里,可以而且应该调用`super.clone()`——即使那个方法本来预期的是一个`Cloneable`对象(否则会抛出一个异常),因为没有人会在我们这种类型的对象上直接调用它。它只有通过一个派生类调用;对那个派生类来说,如果要保证它正常工作,需实现`Cloneable`。
(5) 不实现`Cloneable`来试着防止克隆,并覆盖`clone()`,以产生一个异常。为使这一设想顺利实现,只有令从它派生出来的任何类都调用重新定义后的`clone()`里的`suepr.clone()`。
(6) 将类设为`final`,从而防止克隆。若`clone()`尚未被我们的任何一个上级类覆盖,这一设想便不会成功。若已被覆盖,那么再一次覆盖它,并“抛”出一个`CloneNotSupportedException`(克隆不支持)异常。为担保克隆被禁止,将类设为`final`是唯一的办法。除此以外,一旦涉及保密对象或者遇到想对创建的对象数量进行控制的其他情况,应该将所有构造器都设为`private`,并提供一个或更多的特殊方法来创建对象。采用这种方式,这些方法就可以限制创建的对象数量以及它们的创建条件——一种特殊情况是第16章要介绍的singleton(单例)方案。
下面这个例子总结了克隆的各种实现方法,然后在层次结构中将其“关闭”:
```
//: CheckCloneable.java
// Checking to see if a handle can be cloned
// Can't clone this because it doesn't
// override clone():
class Ordinary {}
// Overrides clone, but doesn't implement
// Cloneable:
class WrongClone extends Ordinary {
public Object clone()
throws CloneNotSupportedException {
return super.clone(); // Throws exception
}
}
// Does all the right things for cloning:
class IsCloneable extends Ordinary
implements Cloneable {
public Object clone()
throws CloneNotSupportedException {
return super.clone();
}
}
// Turn off cloning by throwing the exception:
class NoMore extends IsCloneable {
public Object clone()
throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}
class TryMore extends NoMore {
public Object clone()
throws CloneNotSupportedException {
// Calls NoMore.clone(), throws exception:
return super.clone();
}
}
class BackOn extends NoMore {
private BackOn duplicate(BackOn b) {
// Somehow make a copy of b
// and return that copy. This is a dummy
// copy, just to make the point:
return new BackOn();
}
public Object clone() {
// Doesn't call NoMore.clone():
return duplicate(this);
}
}
// Can't inherit from this, so can't override
// the clone method like in BackOn:
final class ReallyNoMore extends NoMore {}
public class CheckCloneable {
static Ordinary tryToClone(Ordinary ord) {
String id = ord.getClass().getName();
Ordinary x = null;
if(ord instanceof Cloneable) {
try {
System.out.println("Attempting " + id);
x = (Ordinary)((IsCloneable)ord).clone();
System.out.println("Cloned " + id);
} catch(CloneNotSupportedException e) {
System.out.println(
"Could not clone " + id);
}
}
return x;
}
public static void main(String[] args) {
// Upcasting:
Ordinary[] ord = {
new IsCloneable(),
new WrongClone(),
new NoMore(),
new TryMore(),
new BackOn(),
new ReallyNoMore(),
};
Ordinary x = new Ordinary();
// This won't compile, since clone() is
// protected in Object:
//! x = (Ordinary)x.clone();
// tryToClone() checks first to see if
// a class implements Cloneable:
for(int i = 0; i < ord.length; i++)
tryToClone(ord[i]);
}
} ///:~
```
第一个类`Ordinary`代表着大家在本书各处最常见到的类:不支持克隆,但在它正式应用以后,却也不禁止对其克隆。但假如有一个指向`Ordinary`对象的引用,而且那个对象可能是从一个更深的派生类向上转换来的,便不能判断它到底能不能克隆。
`WrongClone`类揭示了实现克隆的一种不正确途径。它确实覆盖了`Object.clone()`,并将那个方法设为`public`,但却没有实现`Cloneable`。所以一旦发出对`super.clone()`的调用(由于对`Object.clone()`的一个调用造成的),便会无情地抛出`CloneNotSupportedException`异常。
在`IsCloneable`中,大家看到的才是进行克隆的各种正确行动:先覆盖`clone()`,并实现了`Cloneable`。但是,这个`clone()`方法以及本例的另外几个方法并不捕获`CloneNotSupportedException`异常,而是任由它通过,并传递给调用者。随后,调用者必须用一个`try-catch`代码块把它包围起来。在我们自己的`clone()`方法中,通常需要在`clone()`内部捕获`CloneNotSupportedException`异常,而不是任由它通过。正如大家以后会理解的那样,对这个例子来说,让它通过是最正确的做法。
类`NoMore`试图按照Java设计者打算的那样“关闭”克隆:在派生类`clone()`中,我们抛出`CloneNotSupportedException`异常。`TryMore`类中的`clone()`方法正确地调用`super.clone()`,并解析成`NoMore.clone()`,后者抛出一个异常并禁止克隆。
但在已被覆盖的`clone()`方法中,假若程序员不遵守调用`super.clone()`的“正确”方法,又会出现什么情况呢?在`BackOn`中,大家可看到实际会发生什么。这个类用一个独立的方法`duplicate()`制作当前对象的一个副本,并在`clone()`内部调用这个方法,而不是调用`super.clone()`。异常永远不会产生,而且新类是可以克隆的。因此,我们不能依赖“抛”出一个异常的方法来防止产生一个可克隆的类。唯一安全的方法在`ReallyNoMore`中得到了演示,它设为`final`,所以不可继承。这意味着假如`clone(`)在`final`类中抛出了一个异常,便不能通过继承来进行修改,并可有效地禁止克隆(不能从一个拥有任意继承级数的类中明确调用`Object.clone()`;只能调用`super.clone()`,它只可访问直接基类)。因此,只要制作一些涉及安全问题的对象,就最好把那些类设为`final`。
在类`CheckCloneable`中,我们看到的第一个类是`tryToClone()`,它能接纳任何`Ordinary`对象,并用`instanceof`检查它是否能够克隆。若答案是肯定的,就将对象转换成为一个`IsCloneable`,调用`clone()`,并将结果转换回`Ordinary`,最后捕获有可能产生的任何异常。请注意用运行期类型识别(见第11章)打印出类名,使自己看到发生的一切情况。
在`main()`中,我们创建了不同类型的`Ordinary`对象,并在数组定义中向上转换成为`Ordinary`。在这之后的头两行代码创建了一个纯粹的`Ordinary`对象,并试图对其克隆。然而,这些代码不会得到编译,因为`clone()`是`Object`中的一个`protected`(受到保护的)方法。代码剩余的部分将遍历数组,并试着克隆每个对象,分别报告它们的成功或失败。输出如下:
```
Attempting IsCloneable
Cloned IsCloneable
Attempting NoMore
Could not clone NoMore
Attempting TryMore
Could not clone TryMore
Attempting BackOn
Cloned BackOn
Attempting ReallyNoMore
Could not clone ReallyNoMore
```
总之,如果希望一个类能够克隆,那么:
(1) 实现`Cloneable`接口
(2) 覆盖`clone()`
(3) 在自己的`clone()`中调用`super.clone()`
(4) 在自己的`clone()`中捕获异常
这一系列步骤能达到最理想的效果。
## 12.3.1 副本构造器
克隆看起来要求进行非常复杂的设置,似乎还该有另一种替代方案。一个办法是制作特殊的构造器,令其负责复制一个对象。在C++中,这叫作“副本构造器”。刚开始的时候,这好象是一种非常显然的解决方案(如果你是C++程序员,这个方法就更显亲切)。下面是一个实际的例子:
```
//: CopyConstructor.java
// A constructor for copying an object
// of the same type, as an attempt to create
// a local copy.
class FruitQualities {
private int weight;
private int color;
private int firmness;
private int ripeness;
private int smell;
// etc.
FruitQualities() { // Default constructor
// do something meaningful...
}
// Other constructors:
// ...
// Copy constructor:
FruitQualities(FruitQualities f) {
weight = f.weight;
color = f.color;
firmness = f.firmness;
ripeness = f.ripeness;
smell = f.smell;
// etc.
}
}
class Seed {
// Members...
Seed() { /* Default constructor */ }
Seed(Seed s) { /* Copy constructor */ }
}
class Fruit {
private FruitQualities fq;
private int seeds;
private Seed[] s;
Fruit(FruitQualities q, int seedCount) {
fq = q;
seeds = seedCount;
s = new Seed[seeds];
for(int i = 0; i < seeds; i++)
s[i] = new Seed();
}
// Other constructors:
// ...
// Copy constructor:
Fruit(Fruit f) {
fq = new FruitQualities(f.fq);
seeds = f.seeds;
// Call all Seed copy-constructors:
for(int i = 0; i < seeds; i++)
s[i] = new Seed(f.s[i]);
// Other copy-construction activities...
}
// To allow derived constructors (or other
// methods) to put in different qualities:
protected void addQualities(FruitQualities q) {
fq = q;
}
protected FruitQualities getQualities() {
return fq;
}
}
class Tomato extends Fruit {
Tomato() {
super(new FruitQualities(), 100);
}
Tomato(Tomato t) { // Copy-constructor
super(t); // Upcast for base copy-constructor
// Other copy-construction activities...
}
}
class ZebraQualities extends FruitQualities {
private int stripedness;
ZebraQualities() { // Default constructor
// do something meaningful...
}
ZebraQualities(ZebraQualities z) {
super(z);
stripedness = z.stripedness;
}
}
class GreenZebra extends Tomato {
GreenZebra() {
addQualities(new ZebraQualities());
}
GreenZebra(GreenZebra g) {
super(g); // Calls Tomato(Tomato)
// Restore the right qualities:
addQualities(new ZebraQualities());
}
void evaluate() {
ZebraQualities zq =
(ZebraQualities)getQualities();
// Do something with the qualities
// ...
}
}
public class CopyConstructor {
public static void ripen(Tomato t) {
// Use the "copy constructor":
t = new Tomato(t);
System.out.println("In ripen, t is a " +
t.getClass().getName());
}
public static void slice(Fruit f) {
f = new Fruit(f); // Hmmm... will this work?
System.out.println("In slice, f is a " +
f.getClass().getName());
}
public static void main(String[] args) {
Tomato tomato = new Tomato();
ripen(tomato); // OK
slice(tomato); // OOPS!
GreenZebra g = new GreenZebra();
ripen(g); // OOPS!
slice(g); // OOPS!
g.evaluate();
}
} ///:~
```
这个例子第一眼看上去显得有点奇怪。不同水果的质量肯定有所区别,但为什么只是把代表那些质量的数据成员直接置入`Fruit`(水果)类?有两方面可能的原因。第一个是我们可能想简便地插入或修改质量。注意`Fruit`有一个`protected`(受到保护的)`addQualities()`方法,它允许派生类来进行这些插入或修改操作(大家或许会认为最合乎逻辑的做法是在`Fruit`中使用一个`protected`构造器,用它获取`FruitQualities`参数,但构造器不能继承,所以不可在第二级或级数更深的类中使用它)。通过将水果的质量置入一个独立的类,可以得到更大的灵活性,其中包括可以在特定`Fruit`对象的存在期间中途更改质量。
之所以将`FruitQualities`设为一个独立的对象,另一个原因是考虑到我们有时希望添加新的质量,或者通过继承与多态性改变行为。注意对`GreenZebra`来说(这实际是西红柿的一类——我已栽种成功,它们简直令人难以置信),构造器会调用`addQualities()`,并为其传递一个`ZebraQualities`对象。该对象是从`FruitQualities`派生出来的,所以能与基类中的`FruitQualities`引用联系在一起。当然,一旦`GreenZebr`a使用`FruitQualities`,就必须将其向下转换成为正确的类型(就象`evaluate()`中展示的那样),但它肯定知道类型是`ZebraQualities`。
大家也看到有一个`Seed`(种子)类,`Fruit`(大家都知道,水果含有自己的种子)包含了一个`Seed`数组。
最后,注意每个类都有一个副本构造器,而且每个副本构造器都必须关心为基类和成员对象调用副本构造器的问题,从而获得“深层复制”的效果。对副本构造器的测试是在`CopyConstructor`类内进行的。方法`ripen()`需要获取一个`Tomato`参数,并对其执行副本构建工作,以便复制对象:
```
t = new Tomato(t);
```
而`slice()`需要获取一个更常规的`Fruit`对象,而且对它进行复制:
```
f = new Fruit(f);
```
它们都在`main()`中伴随不同种类的`Fruit`进行测试。下面是输出结果:
```
In ripen, t is a Tomato
In slice, f is a Fruit
In ripen, t is a Tomato
In slice, f is a Fruit
```
从中可以看出一个问题。在`slice()`内部对`Tomato`进行了副本构建工作以后,结果便不再是一个`Tomato`对象,而只是一个`Fruit`。它已丢失了作为一个`Tomato`(西红柿)的所有特征。此外,如果采用一个`GreenZebra`,`ripen()`和`slice()`会把它分别转换成一个`Tomato`和一个`Fruit`。所以非常不幸,假如想制作对象的一个本地副本,Java中的副本构造器便不是特别适合我们。
(1) 为什么在C++的作用比在Java中大?
副本构造器是C++的一个基本构成部分,因为它能自动产生对象的一个本地副本。但前面的例子确实证明了它不适合在Java中使用,为什么呢?在Java中,我们操控的一切东西都是引用,而在C++中,却可以使用类似于引用的东西,也能直接传递对象。这时便要用到C++的副本构造器:只要想获得一个对象,并按值传递它,就可以复制对象。所以它在C++里能很好地工作,但应注意这套机制在Java里是很不通的,所以不要用它。
- 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 推荐读物