Object.clone()方法执行的是逐字段复制,具体是什么意思呢?

13
在《Effective Java》中,作者提到:
如果一个类实现了Cloneable接口,那么Object的clone方法会返回一个按字段复制的对象;否则,它会抛出CloneNotSupportedException异常。
我想知道的是,他所说的按字段复制是什么意思。这是否意味着如果该类在内存中占用X个字节,它将只复制那段内存?如果是,那么我可以假设原始类的所有值类型都将被复制到新对象中吗?
class Point implements Cloneable{
    private int x;
    private int y;

    @Override
    public Point clone() {
        return (Point)super.clone();
    }
}

如果 Object.clone() 所做的是对 Point 类进行逐个字段的复制,那么我认为我不需要显式地复制字段 xy,因为上面显示的代码将足以克隆 Point 类。也就是说,下面这段代码是多余的:
@Override
public Point clone() {
    Point newObj = (Point)super.clone();
    newObj.x = this.x; //redundant
    newObj.y = this.y; //redundant
}

我对吗?

我知道克隆对象的引用会自动指向原始对象引用所指向的位置,但我不确定值类型具体发生了什么。如果有人能清楚地陈述Object.clone() 的算法规范(用简单的语言),那就太好了。

4个回答

6

是的,逐个字段复制意味着当它创建新的(克隆)对象时,JVM将从原始对象复制每个字段的值到克隆对象中。不幸的是,这意味着您有一个浅层复制。如果您需要深层复制,可以重写克隆方法。

class Line implements Cloneable {

    private Point start;
    private Point end;

    public Line() {
        //Careful: This will not happen for the cloned object
        SomeGlobalRegistry.register(this);
    }

    @Override
    public Line clone() {
        //calling super.clone is going to create a shallow copy.
        //If we want a deep copy, we must clone or instantiate
        //the fields ourselves
        Line line = (Line)super.clone();
        //assuming Point is cloneable. Otherwise we will
        //have to instantiate and populate it's fields manually
        line.start = this.start.clone();
        line.end = this.end.clone;
        return line;
    }
}

关于克隆的另一个重要事项是,被克隆对象的构造函数不会被调用(只有字段被复制)。因此,如果构造函数初始化外部对象或将此对象注册到某个注册表中,则对于克隆对象不会发生这种情况。

我个人更喜欢不使用Java的克隆。相反,我通常创建自己的“复制”方法。


为什么不使用Java的克隆(cloning)方法,而要编写自己的方法?有什么特定的原因吗? - zengr
1
阅读《Effective Java》。有很多条目。 - devoured elysium
@zengr 由于clone()方法不会创建深层副本,因此我不得不自己实现clone方法的主体,并承担Cloneable和CloneNotSupportedException的负担。除此之外,还有一些其他问题,这就是为什么我选择创建自己的方法的原因。它们在《Effective Java》中有很好的解释。 - Parag
原语已经为我们完成了吗?还是我们需要显式地复制它们? - Andrew S

4
newObj.x = this.x; //redundant
newObj.y = this.y; //redundant

没错 - 这些是冗余的,因为它们已经被Object的clone()方法复制了。

将其视为数据副本是正确的。原始类型会被复制,引用也会被复制,所以它们指向同一个对象。例如,

class A implements Cloneable {
  Object someObject;
}

A a = new A();
a.someObject = new Object();

A cloneA = (A)a.clone();
assert a.someObject==cloneA.someObject;

4

这意味着浅拷贝——字段被复制,但是如果你有任何引用,指向的内容不会被复制——你将在旧对象和新克隆对象中拥有两个指向同一对象的引用。然而,对于具有原始类型的字段,字段本身就是数据,因此它们无论如何都会被复制。


正确,但是如果clone方法没有实现为super.clone(),它可能不会遵守契约。 - Stephen C
啊,是的,这绝对是正确的。我注意到在他的例子中,他扩展了Cloneable,这似乎是不正确的——你应该实现它然后覆盖Cloneable方法(也许在Java 1中是通过扩展Cloneable来实现的?)。我想,实现这个意味着“嘿,我有这个特殊的可克隆属性,我可以正确地克隆并在我的克隆方法中进行手动深度复制”。但是,除非你覆盖Object的clone()方法,否则它执行的是浅复制。 - Chris Dennett

1

默认的克隆执行值的浅复制。对于原始值,这已经足够了,不需要额外的工作。

对于对象,浅复制意味着仅复制引用。因此,在这些情况下通常需要进行深层复制。唯一的例外是当引用指向不可变对象时。不可变对象无法改变其表面状态,因此它们的引用可以安全地复制。例如,这适用于String、Integer、Float、枚举(如果没有错误地使其可变)。


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