铁文整理
6.2 对象克隆
当拷贝一个变量时,原始变量与拷贝变量引用同一个对象,如图61所示。这就是说,改变一个变量所引用的对象将会对另一个变量产生影响。
Employee origina1 = new Employee("John Public", 50000);
Employee copy = origiral;
copy.raiseSalary(10); // oops—also changed original
如果创建一个对象的新的copy,它的最初状态与original一样,但以后将可以各自改变各自的状态,那就需要使用clone方法。
Employee copy = origiral.copy();
copy.raiseSalary(10); // OK--original unchanged
不过,事情并没有这么简单。clone方法是Object类的一个protected方法,也就是说,在用户编写的代码中不能直接调用它。只有Employee类才能够克隆Employee对象。这种限制有一定的道理。这里査看一下Object类实现的clone方法。由于这个类对具体的类对象一无所知,所以只能将各个域进行对应的拷贝。如果对象中的所有数据域都属于数值或基本类型,这样拷贝域没有任何问题。但是,如果在对象中包含了子对象的引用,拷贝的结果会使得两个域引用同一个子对象,因此原始对象与克隆对象共享这部分信息。
为了能够说明这种现象,请再看一下第4章中介绍的Employee类。图6-2显示了使用Object类的clone方法克隆Employee对象的结果。可以看到,默认的克隆操作是浅拷贝,它并没有克隆包含在对象中的内部对象。
如果进行浅拷贝会发生什么呢?这要根据具体情况而定。如果原始对象与浅克隆对象共享的子对象是不可变的,将不会产生任何问题。也确实存在这种情形。例如,子对象属于像String类这样的不允许改变的类,也有可能子对象在其生命周期内不会发生变化,既没有更改它们的方法,也没有创建对它引用的方法。
然而,更常见的情况是子对象可变,因此必须重新定义clone方法,以便实现克隆子对象的深拷贝。在列举的示例中,hireDay域属于Date类,这就是一个可变的子对象,
对于每一个类,都需要做出下列判断:
-
默认的clone方法是否满足要求。
-
默认的clone方法是否能够通过调用可变子对象的clone得到修补。
-
是否不应该使用clone。
实际上,选项3是默认的。如果要选择1或2,类必须:
-
实现Cloneable接口。
-
使用public访问修饰符重新定义clone方法。
注释:在Object中,clone方法被声明为protected,因此无法直接调用anObject.clone()。但是,不是所有子类都可以访问受保护的方法吗?不是每个类都是Object的子类吗?值得庆幸的是,受保护访冋的规则极为微妙(参阅第5章)。子类只能调用受保护的clone方法克隆它自己。为此,必须重新定义clone方法,并将它声明为public,这样才能够让所有的方法克隆对象。
在这里,Cloneable接口的出现与接口的正常使用没有任何关系。尤其是,它并没有指定clone方法,这个方法是Object类继承而来的,接口在这里只是作为一个标记,表明类设计者知道要进行克隆处理。如果一个对象需要克隆,而没有实现Cloneable接口,就会产生一个已检验异常(checkedexception)。
注释:Cloneable接口是Java提供的几个标记接口(tagging interface)之一(有些程序员将它们称为标记接口(marker interface))。我们知道,通常使用接口的目的是为了确保类实现某个特定的方法或一组特定的方法,Comparable接口就是这样一个示例。而标记接口没有方法,使用它的惟一目的是可以用instanceof进行类型检查:
if (obj instanceof Cloneable) ...
建议在自己编写程序时,不要使用这种技术。
即使clone的默认实现(浅拷贝)能够满足需求,也应该实现Cloneable接口,将clone重定义为public,并调用super.clone()。下面是一个示例:
class Employee implements C1oneable {
// raise visibility level to public, change return type
public Employee clone() throws CloneNotSupportedException {
return (Employee) super.clone();
}
}
注释:在Java SE 5.0以前的版本中,clone方法总是返回Object类型,而在Java SE 5.0中,允许克隆方法指定返回类型。
刚才看到的clone方法并没有在Object.clone提供的浅拷贝基础上增加任何新功能,而只是将这个方法声明为public。为了实现深拷贝,必须克陸所有可变的实例域。
下面是一个建立深拷贝clone方法的一个示例:
class Employee implements Cloneable {
public Employee clone() throws CloneNotSupportedException {
// call Object.clone()
Employee cloned = (Employee) super.clone();
// clone mutable fields
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
}
只要在clone中含有没有实现Cloneable接口的对象,Object类的clone方法就会抛出一个CloneNotSupportedException异常。当然,Employee和Date类都实现了Cloneable接口,因此不会抛出异常。但是编译器并不知道这些情况,因此需要声明异常:
public Employee clone() throws CloneNotSupportedException
如果将上面这种形式替换成捕获异常呢?
public Employee clone() {
try {
return (Employee) super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
// this won't happen, since we are Cloneable
}
这种写法比较适用于final类,否则最好还是在这个地方保留throws说明符。如果不支持克隆,子类具有抛出CloneNotSupportedException异常的选择权。
必须谨慎地实现子类的克隆。例如,一旦为Employee类定义了clone方法,任何人都可以利用它克隆Manager对象。Employee的克陸方法能够完成这项重任吗?这将取决于Manager类中包含哪些域。在前面列举的示例中,由于bonus域属于基本类型,所以不会出现任何问题。但是,在Manager类中有可能存在一些需要深拷贝的域,或者包含一些没有实现Cloneable接口的域。没有人能够保证子类实现的clone一定正确。鉴于这个原因,应该将Object类中的clone方法声明为protected。但是,如果想让用户调用clone方法,就不能这样做。
在自定义的类中应该实现clone方法吗?如果客户需要深拷贝就应该实现它。有些人认为应该完全避免使用clone,并通过实现其他的方法达到此目的。我们同意这种观点,clone的确显得有点笨拙,但改用其他方法实现这项操作也会遇到同样的问题。至少,克隆的应用并不像人们想像的那样普遍。在标准类库中,只有不到5%的类实现了clone。
在例6-2的程序中,克隆了一个Employee对象,然后,调用了两个改变域值的方法。raiseSalary方法改变了salary域值,setHireDay方法改变了hireDay域值。由于clone实现的是深拷贝,所以对这两个域值的改变并没有影响原始对象。
注释:所有的数组类型均包含一个clone方法,这个方法被设为public,而不是protected。可以利用这个方法创建一个包含所有数组元素拷贝的一个新数组,例如:
int[] luckyNumbers = { 2, 3, 5, 7, 11, 13 };
int[] cloned = (int[]) luckyNumbers.clone();
cloned[5] = 12; // doesn't change luckyNunbers[5]
注释:将在卷II第1章中介绍另一种克隆对象的机制,其中使用了Java的序列化功能。这种机制很容易实现并且也根安全,但效率较低。
例6-2 CloneTest.java
import java.util.*;
/**
* This program demonstrates cloning.
*
* @version 1.10 2002-07-01
* @author Cay Horstmann
*/
public class CloneTest {
public static void main(String[] args) {
try {
Employee original = new Employee("John Q. Public", 50000);
original.setHireDay(2000, 1, 1);
Employee copy = original.clone();
copy.raiseSalary(10);
copy.setHireDay(2002, 12, 31);
System.out.println("original=" + original);
System.out.println("copy=" + copy);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
class Employee implements Cloneable {
public Employee(String n, double s) {
name = n;
salary = s;
hireDay = new Date();
}
public Employee clone() throws CloneNotSupportedException {
// call Object.clone()
Employee cloned = (Employee) super.clone();
// clone mutable fields
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
/**
* Set the hire day to a given date.
*
* @param year
* the year of the hire day
* @param month
* the month of the hire day
* @param day
* the day of the hire day
*/
public void setHireDay(int year, int month, int day) {
Date newHireDay = new GregorianCalendar(year, month - 1, day).getTime();
// Example of instance field mutation
hireDay.setTime(newHireDay.getTime());
}
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
public String toString() {
return "Employee[name=" + name + ",salary=" + salary + ",hireDay="
+ hireDay + "]";
}
private String name;
private double salary;
private Date hireDay;
}