深拷贝工具推荐

78

对于Java集合,是否存在进行深度克隆的实用工具:

  • 数组
  • 列表
  • 映射

注意:最好使用Object.clone()方法而不是序列化来解决问题。我可以确保我的自定义对象将实现clone()方法并且仅使用可克隆的Java标准类...


可能是如何在Java中制作对象的深拷贝?的重复问题。 - Lukas Eder
使用克隆库为我解决了大问题!https://github.com/kostaskougios/cloning - Gaurav
8个回答

66

我认为之前的绿色答案很不好,你可能会问为什么?

  • 它添加了很多代码。
  • 它要求你列出需要复制的所有字段并执行此操作。
  • 在使用clone()时,这对于列表是行不通的。(这是HashMap的clone()所说的:返回此HashMap实例的浅表副本:键和值本身没有被克隆。)所以你最终只能手动完成它(这让我哭了)。

哦,顺便说一句,序列化也很糟糕,你可能需要在很多地方添加Serializable(这也让我哭了)。

那么解决方案是什么呢:

Java Deep-Cloning库 该克隆库是一个小型的、开源的(apache许可证)Java库,可以深度克隆对象。对象不必实现Cloneable接口。实际上,该库可以克隆任何Java对象。如果您不希望缓存对象被修改或者想要创建对象的深层副本,则可以将其用于缓存实现等情况。

Cloner cloner=new Cloner();
XX clone = cloner.deepClone(someObjectOfTypeXX);

请查看https://github.com/kostaskougios/cloning


4
Cloner是一个很棒的库(只是有时它的性能让我哭泣...但我想使用反射也做不到更好)。 - mik01aj
1
在安卓上无法运行... - wieczorek1990
@DaveFar https://dev59.com/t3RB5IYBdhLWcg3wUFnB#666231 - Cojones
1
这个库有一个很大的问题。如果你像那样使用deepClone(),会发生太多的复制!!!我的意思是,如果你有一个“枚举”,在它的主体中声明了一个“抽象”方法,并强制每个常量为该抽象方法提供不同的实现,在从该枚举克隆字段之后,“==”不再适用于这些常量!!这很容易引起问题...库中有一些解决方案。registerFastCloner()和registerImmutable()都可能是解决方案,但我还没有尝试过...而且我不知道它是否可以完全解决! - Mostafa Zeinali
2
据当前维护者 https://github.com/kostaskougios/cloning/issues/105#issuecomment-978225059 表示,Cloner 已不再得到积极的维护。 - AllanT
显示剩余5条评论

20

所有Java中复制对象的方法都有严重的缺陷:

Clone

  1. clone()方法是受保护的,所以除非相关类通过公共方法覆盖它,否则无法直接调用。
  2. clone()不会调用任何构造函数。它将分配内存,分配内部class字段(可以通过getClass()读取),并复制原始字段。

有关clone()更多问题,请参见Joshua Bloch的书 "Effective Java, Second Edition" 的第11项。

Serialize

Serialize甚至更糟糕;它具有许多clone()的缺陷,然后还有更多。Joshua在这个主题上有一个包含四个项目的整章内容。

我的解决方案

我的解决方案是向我的项目添加一个新接口:

public interface Copyable<T> {
    T copy ();
    T createForCopy ();
    void copyTo (T dest);
}

代码看起来像这样:
class Demo implements Copyable<Demo> {
    public Demo copy () {
        Demo copy = createForCopy ();
        copyTo (copy);
        return copy;
    }
    public Demo createForCopy () {
        return new Demo ();
    }
    public void copyTo (Demo dest)
        super.copyTo (dest);
        ...copy fields of Demo here...
    }
}

很不幸,我必须将此代码复制到所有对象中,但它总是相同的代码,因此我可以使用Eclipse编辑器模板。优点:

  1. 我可以决定调用哪个构造函数以及如何初始化哪个字段。
  2. 初始化按照确定性顺序进行(从根类到实例类)
  3. 我可以重用现有对象并覆盖它们
  4. 类型安全
  5. 单例保持单例

对于标准Java类型(如集合等),我使用一个实用程序类来复制它们。这些方法具有标志和回调,因此我可以控制要复制多深。


2
我通过在需要克隆的所有类中实现clone()来做了类似的事情。最大的问题是,如果我有一个集合,我必须自己迭代它并复制它... - Juraj
使用一个辅助函数,该函数接受一个集合并返回一个ArrayList:由于您知道大小,因此将仅分配一次内存,并且ArrayList对于通常的访问方式非常快。 - Aaron Digulla
createForCopy 需要返回一个 Demo。 - TimP
你会考虑展示Eclipse编辑器模板吗? - Michal

17

浅层克隆集合很容易,但如果你想要进行深层克隆,使用库可能比手动编码更好(因为你想要克隆集合中的元素)。

就像这个答案一样,我使用了Cloner库,并在性能测试中与XStream(可以通过序列化然后反序列化来“克隆”)和二进制序列化进行了比较。尽管XStream在序列化/反序列化方面非常快速,但是Cloner在克隆方面要快得多:

0.0851毫秒:xstream(通过序列化/反序列化克隆)
0.0223毫秒:二进制序列化(通过序列化/反序列化克隆)
0.0017毫秒:cloner
* 平均时间克隆一个简单对象(两个字段),不使用默认公共构造函数。运行10,000次。

除了速度快之外,选择cloner的其他原因如下:

  1. 执行任何对象的深层克隆(即使是你自己没有编写的对象)
  2. 无需每次添加字段时更新clone()方法
  3. 可以克隆没有默认公共构造函数的对象
  4. 与Spring一起使用
  5. (优化)不会克隆已知的不可变对象(如Integer,String等)
  6. 易于使用。示例:

    cloner.deepClone(anyObject);


“克隆一个简单对象(两个字段)而且没有默认值的平均时间” – 这看起来不像是“深度克隆”的正确测量标准。一个正确的测试将至少有两个级别的嵌套集合。 - m1ld
2
据当前维护者 https://github.com/kostaskougios/cloning/issues/105#issuecomment-978225059 表示,Cloner 已不再得到积极的维护。 - AllanT

16

我是 cloner lib 的创建者,就是 Brad 展示的那个。这是一个解决方案,可以在不需要编写额外代码(无需可序列化的对象或实现 clone() 方法)的情况下克隆对象。

正如 Brad 所说的那样,它非常快,并且最近我上传了一个更快的版本。请注意,手动实现 clone() 方法比使用 clone lib 更快,但是你需要编写大量的代码。

Cloner lib 在我的缓存实现中工作得非常好,该站点有非常重的流量(~1 百万次请求/天)。每个请求应该克隆约 10 个对象。它非常可靠和稳定。但请注意,克隆并不是没有风险的。该库可以配置为在开发过程中 println 克隆的每个类实例。这样,您可以检查它是否克隆了您认为应该克隆的内容 - 对象图可以非常深,并且可能包含对大量对象的意外引用。使用 clone lib,您可以指示它不要克隆您不想要的对象,例如单例。


这个库看起来不错,稍后会检查一下... - Juraj
使用克隆库,您可以指示它不要克隆您不想要的对象。我尝试过这样做,但无法成功,如何让它不克隆某些字段? - Sudarshan
问候Konstantinos。我正在使用您的库,而不是Apache utils(由于SerializableUtils的转换问题)。您的库没有问题。做得好! - will824
我已经使用你的库很长时间了,真的非常棒!谢谢你!D: - Adams.H

11

深度克隆任意集合的一种通用方式是将其序列化到流中,然后读取到一个新集合中。你将重新生成全新的对象,它们与旧对象没有任何关系,除了是完全相同的副本。

如果你决定采取这种方式,请查看Bruno的答案,其中包含Apache Commons序列化工具类的链接,这些工具类将非常有用。


序列化方案可以,但我想到了一种不需要它的方法。我可以保证我的自定义对象将通过clone()方法正确地进行深层克隆,但是我希望有一个辅助程序可以为标准Java类执行相同的操作... - Juraj
序列化克隆的方式很好,但我有一些不受我控制且不可序列化的字段... - Juraj
这对于测试场景特别有用,其中性能并不是那么关键,而且副本也不需要完全符合功能要求。 - Adam Wise

5

2
我使用了这个克隆库,发现它非常有用。由于它有一些限制(我需要更细粒度的控制克隆过程:哪个字段,在什么上下文中以及如何深度克隆等),所以我创建了一个扩展版本。通过在实体类中注释字段,您可以控制字段的克隆。只是为了让您了解一下,这里是一个示例类:
public class CloneMePlease {
    @Clone(Skip.class)
    String id3 = UUID.randomUUID().toString();

    @Clone(Null.class)
    String id4 = UUID.randomUUID().toString();

    @Clone(value = RandomUUID.class, groups=CustomActivationGroup1.class)
    String id5 = UUID.randomUUID().toString();

    @Clone.List({
            @Clone(groups=CustomActivationGroup2.class, value=Skip.class),
            @Clone(groups=CustomActivationGroup3.class, value=Copy.class)})
    Object activationGroupOrderTest = new Object();

    @Clone(LongIncrement.class)
    long version = 1l;

    @PostClone
    private void postClone(CloneMePlease original, @CloneInject CloneInjectedService service){
         //do stuff with the original source object in the context of the cloned object
         //you can inject whatewer service you want, from spring/guice to perform custom logic here
    }
}

更多细节在这里: https://github.com/mnorbi/fluidity-cloning 此外,如果需要的话,还有一个针对 Hibernate 的特定扩展。

0

使用序列化和反序列化,但请注意,此方法仅适用于没有瞬态字段的可序列化类。此外,您的单例将不再是单例。


1
在运行时或程序的不同运行之间,使用序列化和反序列化来克隆内存对象是一个不好的想法。关于为什么这样做不好的更多信息,请搜索谷歌:“为什么序列化和反序列化不好”。 - Eric Leschinski

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