Cloneable克隆

现有两个类People以及Student,他们之间是继承关系

class People 
{
    private String name;
    private int age;
    private String[] parents = new String[2];

    public People (String name, int age, String[] parents)
    {
        this.name = name;
        this.age = age;
        this.parents = parents;
    }

    public String getName ()
    {
        return this.name;
    }

    public int getAge ()
    {
        return this.age;
    }

    public String[] getParents()
    {
        return this.parents;
    }

    public void setName (String name)
    {
        this.name = name;
    }

    public void setAge (int age)
    {
        this.age = age;
    }

    public void setParents (String parentName, int index)
    {
        this.parents[index] = parentName;
    }
}

class Student extends People
{
    private String school;

    public Student (String name, int age, String[] parents, String school)
    {
        super(name, age, parents);
        this.school = school;
    }

    public void setSchool (String school)
    {
        this.school = school;
    }

    public String getSchool ()
    {
        return this.school;
    }
}

当对象引用的变量建立副本时,原变量以及副本都是同一个对象的引用。

var original = new People("Paul", 35, new String[]{"Green", "Pink"});
var copy = original;

copy.setAge(36); // also changed original

如果希望copy是一个新对象,它的初始状态与original相同,但是之后它们各自会有自己不同的状态,这种情况下就应该使用clone方法。

但clone方法是Object的一个protected方法,说明这里我不可以直接调用这个方法。只有People类内部可以调用。(这个限制是有原因的,后面会解释)。

浅拷贝

Object提供的clone是使用浅拷贝,浅拷贝并不会克隆对象中引用的其他对象。如果对象中的所有数据字段都是数值或其他基本类型,拷贝这些字段没有任何问题。但是如果对象包含子对象的引用,拷贝字段就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对象仍然会共享一些信息。

下面是在People中实现浅拷贝的代码:

class People implements Cloneable
{
    ......

    public People clone () throws CloneNotSupportedException
    {
        return (People) super.clone();
    }
}

在这里,Cloneable接口的出现与接口的正常使用并没有关系。具体来说,它没有指定clone方法,这个方法是从Object类继承。这个接口只是作为一个标记,指示类设计者了解克隆过程。对象对克隆很“偏执”,如果一个对象请求克隆,但是么有实现这个接口,就会生成一个检查型异常。(标记接口不包含任何方法;它唯一的作用就是允许在类型查询中使用instanceof if (obj instanceof Cloneable)...

如果原对象和克隆对象没有共享的子对象,或原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的。如子对象属于一个不可变的类,如String,就是这种情况。或者在对象的生命期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没办法会生成它的引用,这种情况同样是安全的。

但我们这里有一个子对象引用String[],看看会出现什么情况

var original = new People("Paul", 35, new String[]{"Green", "Pink"});
var copy = original.clone();

copy.setAge(36); 
copy.setParents("Smart", 0);
copy.setParents("Love", 1);
System.out.println(original.getAge()); // 35
System.out.println(Arrays.toString(original.getParents())); // [Smart, Love]

深拷贝

可以看出,copy修改了parents,也会影响到original的parents。所以,对于这种含可变子对象的情况,我们应该使用深拷贝,同时克隆所有子对象。

我们修改clone方法,完成深拷贝。

public People clone () throws CloneNotSupportedException
{
    People p = (People) super.clone();
    p.parents = this.parents.clone();
    return p;
}

至此,深拷贝就完成了。上面的测试代码不会再打印[Smart,Love]而是[Green, Pink]。

知识扩展:所有数组类型都有一个公共的clone方法,而不是受保护的。可以用这个方法建立一个新数组,包含原数组所有元素的副本。

克隆与继承

必须当心子类的克隆。例如,一旦People类定义了clone方法,任何人都可以用它来克隆Student对象。People克隆方法可以完成工作么?这取决于Student字段,这里是没问题的,因为Student不含可变子对象。但是Student可能会有需要深拷贝或不可克隆的字段。不能保证子类的实现者一定会修正clone方法让它正常工作。处于这个原因,在Object类中的clone方法声明为protected。不过,如果你希望类用户调用clone,就不能这么做。