有效的Java第11项:明智地覆盖clone方法

3

如果一个类有一个数组字段,Josh说如果clone方法只是return super.clone(),则生成的类实例将具有原始字段中的正确值,但其数组字段将引用与原始类实例相同的数组。修改原始实例将破坏不变量,反之亦然。

他举了自定义堆栈实现的示例,我使用了一个简单的学生类

class Student implements Cloneable {
    private String name;
    private int age;
    private int[] marks = {90, 70, 80};

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

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

    public void setMarks(int[] marks) {
        this.marks = marks;
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    protected Student clone() throws CloneNotSupportedException {
        return (Student) super.clone();
    }

    @Override
    public String toString() {
        return "Student - Name : " + name + " Age : " + age + " Marks : " + Arrays.toString(marks);
    }
}

请注意:在我的克隆方法重写中,我没有调用数组字段的clone()方法。
然后我这样做了:
public class CloningDemo {
    public static void main(String[] args) {
        Student s1 = new Student("Mohit", 30);
        Student s2 = null;
        try {
            s2 = s1.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        System.out.println("S1 : " + s1);
        System.out.println("S2 : " + s2);
        System.out.println("Updating the clone...");
        s2.setName("Rohit");
        s2.setAge(29);
        s2.setMarks(new int[]{10, 29, 30});
        System.out.println("S1 : " + s1);
        System.out.println("S2 : " + s2);
        System.out.println("Updating the array elements in Original...");
        s1.setMarks(new int[]{10, 10, 10});
        System.out.println("S1 : " + s1);
        System.out.println("S2 : " + s2);
    }
}

输出:

S1 : Student - Name : Mohit Age : 30 Marks : [90, 70, 80]
S2 : Student - Name : Mohit Age : 30 Marks : [90, 70, 80]
Updating the clone...
S1 : Student - Name : Mohit Age : 30 Marks : [90, 70, 80]
S2 : Student - Name : Rohit Age : 29 Marks : [10, 29, 30]
Updating the array elements in Original...
S1 : Student - Name : Mohit Age : 30 Marks : [10, 10, 10]
S2 : Student - Name : Rohit Age : 29 Marks : [10, 29, 30]

我想知道在原始实例中更改数组是否也会更改克隆中的数组,因为我之前提到过“数组字段将引用与原始实例相同的数组”

使用我的克隆实现,我应该在克隆体s2中看到更改。正确的实现方式应该是:

@Override
    protected Student clone() throws CloneNotSupportedException {
        Student student = (Student) super.clone();
        student.marks = marks.clone();  // I am not doing this in my code.
        return student;
    }

我理解错了吗?可以有人解释一下发生了什么吗?
谢谢, ~Mohit
2个回答

10

当您调用s1.setMarks(new int[]{10, 10, 10});时,您正在创建一个全新的数组,并将其引用写入s1的变量marks中。因此,s1s2引用两个不同的数组。

如果您拥有以下方法:

public void setMark(int mark, int pos) {
    marks[pos] = mark;
}

在类Student中执行以下代码:

System.out.println("Updating the array element in Original...");
s1.setMark(999, 0);
System.out.println("S1 : " + s1);
System.out.println("S2 : " + s2);

然后你会发现,这也影响了 s2

Updating the array elements in Original...
S1 : Student - Name : Mohit Age : 30 Marks : [999, 70, 80]
S2 : Student - Name : Rohit Age : 29 Marks : [999, 70, 80]

(不要忘记注释掉s2.setMarks(new int[]{10, 29, 30});这一行,因为这行代码也会创建一个新的数组引用并且删除了s1s2之间的(数组)绑定关系)

这种行为可以用一个“真实世界的例子”来描述:

想象你和朋友各拿住一根绳子的两端。这条绳子代表你们都在引用的Array。如果你的朋友拉那根绳子(改变数组中的某个值),你会注意到的。同样,如果你拉那根绳子,你的朋友也会注意到。

通过调用s1.setMarks(new int[]{...});,你的朋友会得到一条新的绳子,并会放下旧绳子。如果他拉新绳子,你不会注意到,因为你们引用的是不同的绳子。通过调用s2.setMarks(new int[]{...});,你也会得到一条新的绳子并放下旧绳子。这时第三个朋友,叫做Garbage Collector,就会把旧绳子拿走丢弃,因为没有人再使用它了。但是这个朋友有点懒,不能保证他会立即处理。


2
一个类型为int[]的变量可以用来封装以下四种不同的内容:
  1. 永远不会被修改的数组的内容。

  2. 可能被修改的数组的内容,并且没有任何引用指向该数组,该变量的所有者也不知道它们存在。

  3. 可能被修改的数组的标识符,且该数组归其他人所有。

  4. 可能被修改的数组的标识符,该数组归该变量的所有者所有,但可能存在其他引用。

clone()方法不需要克隆第一种类型的数组,但除了轻微的性能成本外,克隆这种数组很可能是无害的。但是,clone()方法必须克隆第二种类型的数组并避免克隆第三种类型的数组。拥有第四种类型数组的对象通常不应该实现clone()
不清楚您的代码是否真的希望将数组视为第一种或第三种类型;在这两种情况下,都不需要在clone方法中克隆数组。使用数组类型变量时,第二种模式最常见,但您特定的用例不适用于此。
对于每个数组类型变量,请确定适用的四种情况之一,这将清楚地说明您应该如何进行clone。请注意,如果不能将数组分类为其中之一,则代码可能存在问题,应该在担心clone之前进行修复。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接