Java中的Cloneable是什么?如何使用它?

110
我想了解以下内容:
1. Cloneable 意味着我们可以通过实现 Cloneable 接口来获得对象的克隆或拷贝。这样做的优缺点是什么?
2. 如果对象是一个组合对象,递归克隆是如何发生的?
6个回答

172
你首先应该知道的是,不要使用 Cloneable
使用 Cloneable 实现克隆非常困难,而且这种努力并不值得。
相反,使用其他选项,如 apache-commons 的SerializationUtils(深度复制)或BeanUtils(浅层复制),或者简单地使用一个拷贝构造函数。
请参见 这里,了解Josh Bloch对使用 Cloneable 进行克隆的看法,其中解释了这种方法的许多缺点。(Joshua Bloch 是Sun公司的员工,并领导开发了许多Java功能。)

1
我将Bloch的话链接在一起(而不是引用他们) - Bozho
3
请注意,Block建议不要使用Cloneable接口。但他并没有说不要使用克隆(至少我希望是这样)。有很多简单的实现克隆的方法,比如使用反射的SerializationUtils或BeanUtils类远不如其他方法高效。请参考下面我的帖子,其中有一个例子。 - Charles
在定义接口时,复制构造函数的替代方法是什么?只需添加一个复制方法即可吗? - benez
@benez 我会说是的。自从Java-8以来,您可以在接口中拥有static方法,因此只需提供一个static WhatEverTheInterface copy(WhatEverTheInterface initial)即可?但我想知道这给您带来了什么,因为在克隆时从对象复制字段,但接口仅定义方法。能否解释一下? - Eugene
很抱歉,我要对此进行负评,因为这是一条误导性的建议。SerializationUtils 是昂贵的,并且在它们自己的 Java 文档中有充分的记录,此外问题是如何使用 cloneable,应该给出正确的示例而不是错误的建议。我同意可以添加免责声明,但是不使用 cloneable 是完全错误的答案。 - MG Developer
我希望能够听到Java官方宣布废弃该功能,而不是一个架构师声称它已经失效。如果你使用得当,我个人并不认为它已经失效。 - MG Developer

43

Cloneable接口本身只是一个标记接口,也就是说,它并没有定义clone()方法。

它所做的事情是改变受保护的Object.clone()方法的行为。对于没有实现Cloneable的类,该方法将抛出CloneNotSupportedException异常;而对于实现了Cloneable的类,则执行基于成员的浅复制。

即使这是你要寻找的行为,你仍然需要实现自己的clone()方法才能使其公开可用。

在实现自己的clone()方法时,思路是从super.clone()创建的对象开始,这个对象保证是正确类的实例,然后在必要时进行任何额外字段的填充,以避免浅复制。在clone()中调用构造函数可能会有问题,因为这会破坏继承,如果子类想要添加自己的额外的可克隆逻辑,那么调用super.clone()会在这种情况下得到一个错误类的对象。

然而,这种方法会绕过在构造函数中定义的任何逻辑,这可能会潜在地引起问题。

另一个问题是,任何忘记重写clone()方法的子类都将自动继承默认的浅复制,这在可变状态的情况下很可能不是你想要的(此时源对象和克隆对象将共享可变状态)。

由于这些原因,大多数开发人员不使用Cloneable,并简单地实现一个复制构造函数。

有关Cloneable的更多信息和潜在陷阱,我强烈推荐 Joshua Bloch 的《Effective Java》一书。


13
  1. 克隆调用了一种超语言的方式来构建对象 - 不需要构造函数。
  2. 克隆需要你以某种方式处理CloneNotSupportedException - 或者要求客户端代码进行处理。
  3. 好处很小 - 你只是不必手动编写复制构造函数。

因此,请明智地使用Cloneable。与您需要应用于正确执行所有操作的努力相比,它并没有提供足够的好处。


正如Bozho所说,不要使用Cloneable。相反,使用复制构造函数。http://www.javapractices.com/topic/TopicAction.do?Id=12 - Bane
@Bane,如果您不知道要克隆的对象类型,那么该如何知道调用哪个类的复制构造函数? - Steve Kuo
@Steve:我不明白。如果你要克隆一个对象,我认为你已经知道它是什么类型——毕竟,你手头有你计划克隆的对象。如果存在这样一种情况,即你的对象已经失去了特定类型而变成更通用的类型,难道不能使用简单的“instance of”进行评估吗? - Bane
4
@Bane:假设您有一个对象列表,它们都派生自类型A,可能有10种不同的类型。您不知道每个对象的类型。在这种情况下使用instanceof是非常糟糕的选择。如果您添加了另一种类型,则每次添加时都必须添加另一个instanceof测试。而且,如果派生类在您无法访问的另一个包中怎么办?克隆是一种常见的模式。是的,Java实现很糟糕,但有很多方法可以解决这个问题。复制构造函数并不是等效的操作。 - Charles
@Charles:在没有详细示例的情况下,又缺乏最近处理这种问题的经验,我必须推迟到Bloch。第11条。它很长,有点难读,但基本上是说“尽可能避免使用可克隆接口,拷贝构造函数是你的朋友”。 - Bane

10
克隆是一种基本的编程范例。虽然Java在许多方面实现得不好,但这并不会降低对克隆的需求。而且,很容易实现克隆,无论你希望它如何工作,浅层、深层、混合等都可以。甚至可以使用函数名clone,并且如果愿意的话,可以不实现Cloneable接口。
假设我有类A、B和C,其中B和C是从A派生出来的。如果我有一个像这样的类型为A的对象列表:
ArrayList<A> list1;

现在,该列表可以包含类型为A、B或C的对象。您不知道对象的类型。因此,您不能像这样复制列表:
ArrayList<A> list2 = new ArrayList<A>();
for(A a : list1) {
    list2.add(new A(a));
}

如果对象实际上是B或C类型,你将无法获得正确的副本。如果A是抽象的呢?现在,有些人建议这样做:
ArrayList<A> list2 = new ArrayList<A>();
for(A a : list1) {
    if(a instanceof A) {
        list2.add(new A(a));
    } else if(a instanceof B) {
        list2.add(new B(a));
    } else if(a instanceof C) {
        list2.add(new C(a));
    }
}

这是一个非常不好的想法。假如您添加了新的派生类型该怎么办?假如B或C在另一个包中,而您在该类中没有访问它们的权限呢?
您应该这样做:
ArrayList<A> list2 = new ArrayList<A>();
for(A a : list1) {
    list2.add(a.clone());
}

许多人指出Java基本克隆实现的问题。但是,可以通过以下方式轻松解决:

在类A中:

public A clone() {
    return new A(this);
}

在B班:

@Override
public B clone() {
    return new B(this);
}

在C类中:

@Override
public C clone() {
    return new C(this):
}

我没有实现Cloneable接口,只是使用了同样的函数名称。如果你不喜欢这个,就把它命名为其他名称。


1
我在回复你的评论后才看到这个问题;现在我明白你的意思了,但是有两点需要注意:1)OP特别询问了使用Cloneable(而不是克隆的通用概念),2)你在试图区分复制构造函数和克隆的通用概念时有些过于苛求。你传达的想法是正确的,但本质上你只是在使用一个复制构造函数。 ;) - Bane
虽然我想说我同意你的方法,即包括一个A#copyMethod(),而不是强制用户直接调用复制构造函数。 - Bane
1
我正在点赞这篇文章,因为它准确地提供了所问问题的示例。得票最高的答案并不总是正确的建议。SerializationUtils和类似的BeanUtils对性能非常不利。复制构造函数是双赢的选择,但如果实现得当,克隆本身也不错。 - MG Developer

5

A) 克隆和拷贝构造函数相比,并没有太多的优势。可能最大的优点是能够创建一个精确相同的动态类型的新对象(假设声明类型是可克隆的并且具有公共的克隆方法)。

B) 默认的克隆创建一个浅拷贝,除非您的克隆实现更改了它。这可能很困难,特别是如果您的类有final字段。

Bozho是正确的,克隆可能很难掌握。拷贝构造函数/工厂将满足大多数需求。


0

Cloneable有哪些缺点?

如果要复制的对象具有组合,则克隆非常危险。在这种情况下,您需要考虑以下可能的副作用,因为克隆会创建浅层副本:

假设您有一个对象来处理与数据库相关的操作。假设该对象具有Connection对象作为其中一个属性。

因此,当有人创建originalObject的克隆体时,被创建的对象是cloneObject。在这里,originalObjectcloneObject对于Connection对象持有相同的引用。

假设originalObject关闭了Connection对象,那么现在cloneObject将无法工作,因为它们之间共享了connection对象,并且实际上是由originalObject关闭的。

如果要克隆具有IOStream属性的对象,则可能出现类似的问题。

如果对象是复合对象,递归克隆如何发生?

Cloneable执行浅拷贝。这意味着原始对象和克隆对象的数据将指向同一引用/内存。 相反,在深拷贝的情况下,原始对象的内存中的数据被复制到克隆对象的内存中。


你的最后一段非常混乱。Cloneable 不执行复制,Object.clone 执行复制。"从原始对象的内存中复制数据到克隆对象的内存" 正是 Object.clone 所做的。如果要描述深度复制,你需要谈论引用对象的内存。 - aioobe

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