10.5 对象复制

在程序运行过程中,可能会需要复制一个对象。例如,根据防御式编程(defensive programming)的实践,当一个方法将一个内部使用的对象返回给调用者时,最好先把该对象复制一份,将复制的对象返回给调用者。如果不进行复制,则调用者和当前对象使用的是同一个对象。如果调用者对这个对象进行了修改,那么会影响当前对象的内部状态。对象复制功能还可以在其他场景下发挥作用。Object类中的clone方法和java.lang.Cloneable接口用来提供标准的对象复制功能。

要实现对象复制功能,需要Object类的clone方法和Cloneable接口配合使用。Cloneable是一个不包含任何方法的标记接口,它的作用是作为复制功能相关的标记。如果一个类实现了Cloneable接口,就说明可以通过Object类的clone方法提供的默认实现来对该类的实例对象包含的域进行复制。如果调用clone方法的对象的Java类没有实现Cloneable接口,那么clone方法会直接抛出java.lang.CloneNotSupportedException异常。代码清单10-9给出了一个简单的可以进行复制操作的Java类。如果删去CloneableObject类对Cloneable接口的实现,则代码运行时会抛出CloneNotSupportedException异常。按照惯例,实现了Cloneable接口的Java类需要提供一个公开的clone方法来覆写Object类中的clone方法,这是因为Object类中的clone方法被声明为受保护的,如果不进行覆写,外部的对象无法访问到clone方法。

代码清单10-9 可以进行复制操作的Java类的示例


public class CloneableObject implements Cloneable{

public Object clone(){

try{

return super.clone();

}catch(CloneNotSupportedException e){

throw new Error(e);//不会发生该异常

}

}

public static void main(String[]args){

CloneableObject obj=new CloneableObject();

obj.clone();

}

}


Java中的对象复制操作的这种设计,不太符合一般的习惯做法。一般认为在Cloneable接口中有一个clone方法,如果Java类需要提供复制功能,就实现Cloneable接口并编写对应的clone方法的实现。但是Java的实现并不是这样的。即便一个Java类实现了Cloneable接口,也不表示可以调用该类的clone方法进行复制操作,一种可能是该Java类并没有提供公开的clone方法,因此无法调用clone方法。

Object类的clone方法复制对象的做法是对当前对象中所有的实例域进行逐一复制。先创建一个新的对象,再把新对象中所有的实例域的值初始化成原始对象中对应域的当前值。该方法一般是使用原生代码实现的。代码清单10-10给出了使用Object类的clone方法的示例。ToBeCloned类的clone方法直接通过Object类的clone方法来实现。ToBeCloned类包含一个int类型的实例域value。在cloneObject方法中先创建了一个ToBeCloned类的对象obj。在调用对象obj的clone方法时,先创建一个ToBeCloned类的对象,再把该对象中实例域value的值初始化为对象obj中value的当前值。这个新创建的ToBeCloned类的对象clonedObj,就是复制对象obj之后的结果。

代码清单10-10 Object类的clone方法的使用示例


class ToBeCloned implements Cloneable{

private int value=0;

public void setValue(int value){

this.value=value;

}

public int getValue(){

return this.value;

}

public Object clone(){

try{

return super.clone();

}catch(CloneNotSupportedException e){

throw new Error(e);

}

}

}

public class SimpleClone{

public void cloneObject(){

ToBeCloned obj=new ToBeCloned();

obj.setValue(1);

ToBeCloned clonedObj=(ToBeCloned)obj.clone();

System.out.println(clonedObj.getValue());

}

}


可以将Object类的clone方法的实现看成是为原始对象创建了一个浅拷贝。如果对象中只包含值为基本类型或不可变对象的域,浅拷贝就足够了。如果对象中某些域的值为可变对象,浅拷贝就不能满足需求。因为所复制出来的对象的域与原始对象的域使用相同的对象引用,指向的是同一个对象,相当于在两个对象中对同一个对象进行处理,会产生潜在的问题。代码清单10-11给出了一个浅拷贝可能带来问题的示例。类MutableObject中包含一个实例域counter,是Counter类的对象。Counter类的对象是可变的,有自己不同的内部状态值value。类MutableObject的clone方法只是简单地复用了Object类中的clone方法。在进行复制操作之后,原始对象obj和复制出来的新对象clonedObj内部的counter域都指向对象obj中的Counter类的对象,所以虽然只有一次对clonedObj对象的increase方法的调用,但是clonedObj对象的getValue方法的返回值却为3,这是因为通过对象obj的increase方法所做的修改同样影响了clonedObj对象。

代码清单10-11 浅拷贝可能带来的问题的示例


class Counter{

private int value=0;

public void increase(){

value++;

}

public int getValue(){

return value;

}

}

class MutableObject implements Cloneable{

private Counter counter=new Counter();

public void increase(){

counter.increase();

}

public int getValue(){

return counter.getValue();

}

public Object clone(){

try{

return super.clone();

}catch(CloneNotSupportedException e){

throw new Error(e);

}

}

}

public class MutableObjectClone{

public void cloneObject(){

MutableObject obj=new MutableObject();

obj.increase();

MutableObject clonedObj=(MutableObject)obj.clone();

clonedObj.increase();

obj.increase();

System.out.println(clonedObj.getValue());//值为3

}

}


要解决这个浅拷贝的问题,就要提供自己的深拷贝的实现。虽然Object类的clone方法已经不能满足需求,但是仍然可以作为实现的基础。Object类的clone方法已经对类中的基本类型和不可变对象的域进行了处理,只要在这基础上添加对可变对象的域的处理即可。要对代码清单10-11中的类进行修改,先要让Counter类实现Cloneable接口,并提供对应的公开的clone方法。由于Counter类中只包含一个int类型的域,因此可以直接调用Object类中的clone方法。而对于MutableObject类中的clone方法,将其修改成代码清单10-12中的实现。在这个实现中,先调用Object类的clone方法得到复制之后的对象obj作为基础,再对MutableObject类中的可变对象的域counter进行处理。使用clone方法对原始的counter对象进行复制,再修改对象obj中counter域的值,使之指向复制出来的counter对象。经过这样的修改,obj对象中的counter域引用的是一个新的Counter类的对象。

代码清单10-12 深拷贝的实现方式的示例


public Object clone(){

MutableObject obj;

try{

obj=(MutableObject)super.clone();

obj.counter=(Counter)counter.clone();

return obj;

}catch(CloneNotSupportedException e){

throw new Error(e);

}

}


这种深拷贝操作要求对当前对象的实例域所引用的可变对象都以递归的方式进行复

制。其中所涉及的每个对象的类都应该实现Cloneable接口,并提供正确的clone方法的实现。

进行对象复制的另外一个做法是使用复制构造方法,即用一个已有的对象去构造另外一个对象。复制构造方法相对于clone方法来说更加容易使用,也不容易出错。如果一个Java类需要提供公开API来进行复制操作,使用复制构造方法是更好的选择。复制构造方法的一个好处是可以在功能相似而类型不同的对象之间传递数据。可以在Java的集合类框架中看到相关的示例。比如java.util.ArrayList的复制构造方法可以接受任何java.util.Collection接口的实现对象作为参数值。可以很容易地把一个java.util.HashSet类的对象转换成ArrayList类的对象。使用clone方法是无法做到这一点的。

代码清单10-13中的User类除了一般的构造方法之外,还提供了一个复制构造方法。当需要复制一个已有的User类的对象时,可以使用已有的对象作为参数来调用复制构造方法。创建出来的新对象就是已有对象的副本。

代码清单10-13 包含复制构造方法的Java类的示例


public class User{

private String name;

private String email;

public User(String name, String email){

this.name=name;

this.email=email;

}

public User(User user){

this.name=user.getName();

this.email=user.getEmail();

}

public String getName(){

return this.name;

}

public String getEmail(){

return this.email;

}

}