Java中为什么需要克隆对象?

5

我正在阅读Joshua Bloch的Effective Java。在其中,他谈到不要使用Clonable接口。作为一个新手,我的问题是,什么情况下需要在代码中使用克隆?有人能给我一个具体的例子,以便我能够理解这个概念吗?


为什么要暂停?我不知道克隆的原因。如果有一个例子,那会帮助我理解克隆对象到底是做什么的。 - Horse Voice
我同意。它的作用是生成对象的按位副本,除非它的基类或任何成员具有clone()方法,在这种情况下,它将为这些部分执行它们的操作。但自1997年以来,我从未在生产Java中使用过clone(),也从未使用所谓的“复制构造函数”。从未遇到过真正需要它的要求。 - user207421
对象的按位复制是什么意思? - Horse Voice
@TazMan 如果您检查内存中的原始对象和其克隆体,您会看到两个完全相同的内存区域,它们看起来完全一样。 - awksp
@TazMan 我的意思是每个位都被完全复制。这不是显而易见的吗?它本来就是这样设计的。 - user207421
4个回答

6
一个clone()接口提供了创建对象的"浅"副本的机制。也就是说, 默认情况下会为副本分配更多的内存,并将原始对象的每个部分复制到副本中。相比之下,将一个对象实例简单地赋值给一个变量将导致对同一对象的另一个引用。虽然克隆对象本身是真正的副本,但它包含的元素默认情况下是指向原始对象所指向的元素的引用。如果需要真正的“深层”副本,则需要专门设置clone()方法来创建其成员的克隆。
一个可能使用clone()接口的案例是实现对象的版本历史记录,以允许将其回滚到旧版本。这可以在事务性系统(例如数据库)中使用。
另一个使用案例是实现写时复制,在对象的用户仅提供只读版本时,这可能很有用。

*感谢和赞扬newacct纠正克隆描述。


1
请纠正我,但我认为对象赋值不是浅拷贝。实际上,这只是另一个指向同一对象的指针。克隆在哪里? - Horse Voice
你所称之为的“深层复制”通常被人们称作“浅层复制”。 - newacct
1
等等,什么?从Java API中:“这意味着复制组成被克隆对象的内部‘深层结构’的任何可变对象,并用对这些对象的引用替换为对副本的引用。”听起来像是深拷贝。http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#clone%28%29 - wickstopher
@cecilg23:同一文档中的后续段落如下:“因此,该方法执行的是该对象的“浅拷贝”,而非“深拷贝”操作。” - jxh
好的,我现在明白你的意思了,你是对的。但是我的观点是,覆盖基本的clone()方法(这就是问题所在)的目的是通过设计确保执行深度复制。如果你只是实现Cloneable接口,仅此而已,那么你得到的是浅层复制,因为该接口不要求你覆盖clone()方法。 - wickstopher

2
从维基百科关于原型模式的介绍中:
“...当您想要在运行时创建另一个与您克隆的对象是一个真正的副本时,您需要使用clone()来克隆对象。 真正的副本意味着新创建的对象的所有属性都应该与您克隆的对象相同。如果您可以使用new实例化类,那么您将获得一个带有所有属性的对象作为它们的初始值。例如,如果您正在设计一个用于执行银行账户交易的系统,那么您将希望复制保存您账户信息的对象,对其进行交易,然后用修改后的对象替换原始对象。在这种情况下,您将希望使用clone()而不是new。”

1
在Java中克隆对象有非常好的理由。考虑以下代码:
MyObj first = new MyObj(someOwner, someTag, someInt);
MyObj second = first;

在这种情况下,我们只是复制了那个对象的引用(内存地址)。变量 firstsecond 都指向 MyObj 类的同一个实例。clone() 方法所要实现的是所谓的深拷贝。当然,这取决于具体的实现:clone 方法的开发者需要确保实现了深拷贝。因此,代码如下:
MyObj third = (MyObj) first.clone();

在这种情况下,clone() 的作用是遍历所有的first实例成员并复制/克隆它们,并使用这些值创建一个全新的MyObj实例。然后返回对新实例的引用,因此third是first的副本,而不仅仅是对同一实例的引用。
回应您在评论中的问题,它取决于您的实现是否使成员变量的新克隆,还是仅复制引用。考虑以下MyObj示例类的实现,假设还存在Person和NameTag类。如果您克隆MyObj对象,您可能希望新克隆引用与原始Owner实例相同,但深度复制NameTag(当然,这只是使用想象类的示例)。这将表示MyObj实例和NameTag实例之间的一对一关系,以及Owner实例和MyObj实例之间的一对多关系。以下代码考虑了您问题中提到的两种情况:
class MyObj implements Cloneable {

    private Person owner;
    private NameTag tag;
    private int size; 

    public MyObj(Person owner, NameTag tag, int size) {
         this.owner = owner;
         this.tag = tag;
         this.size = size;
    }

    public Object clone() {

        MyObj result;

        //call all super clone() methods before doing class specific work
        //this ensures that no matter where you are in the inheritance heirarchy,
        //a deep copy is made at each level.
        try {
            result = (MyObj) super.clone();
        }
        catch (CloneNotSupportedException e) {
            throw new RuntimeException
            ("Super class doesn't implement cloneable");
        }

        //work specific to the MyObj class
        result.owner = owner;     //We want the reference to the Person, not a clone
        result.tag = tag.clone()  //We want a deep copy of the NameTag
        result.size = size;       //ints are primitive, so this is a copy operation    

        return result;
}

如果“first”实例包含其他引用变量和列表,那么执行first.clone()是否会对它们进行克隆? - Horse Voice
我更新了我的答案,更详细地回答了那些问题。如果这样更清楚,请告诉我! - wickstopher
但是它比MyObj有一个“复制构造函数”(一个接受MyObj的构造函数)并使用它来创建对象更好吗? - newacct
其实,这两种方法都没有比另一种更好。只是克隆是标准的Java范例,而拷贝构造函数则是C++的范例。无论在哪种语言中,这两种方法都能很好地工作。但是,如果在Java中正确实现了克隆,它会具有一些额外的容错机制,以确保跨平台的兼容性(如上所述)。 - wickstopher

1

其中一个例子是参数的防御性拷贝(Effective Java第39条:必要时进行防御性拷贝)

class X {
  private byte[] a;

  X(byte[] a) {
      this.a = a.clone();
  }
...

这允许什么? - Horse Voice
1
它确保新的 X 实例有传入字节数组的独立副本作为其实例成员,而不是对传入对象的引用。 - wickstopher

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