Java对象的深度克隆(非bean)

7
我目前正在处理的项目中有许多对象进行了序列化,以获得现有对象的深度拷贝。这在单次调用时是可以正常工作的,但在某些情况下,组件之间存在多次调用,每次可能达到100、200甚至1000次,在这种情况下,我们面临性能问题。
历史原因是为了防止两个不同的组件在不同的功能下对同一对象进行更改,例如Swing UI中的更改应该在保存或同步按钮被按下之前不会更改后端的对象值。
我们的代码库非常庞大,我认为如果我基于反射编写克隆,它将比序列化更快,但由于对象层次结构复杂或其他原因,此方法甚至更慢。
我还尝试使用CloneUtils(sourceforge项目),它也更慢(我们根本没有使用Hibernate)。Spring BeanUtils不是一个选择(我从文档中推断它只使用bean,即内省,并且如果使用它,如果任何字段使用非标准访问器公开,我们将无法复制这些字段)。
有人有什么想法,可以在保持不同副本的同时提高性能。我们有一种选项,如果我们提供自己的复制方法,而不是序列化,那么将加速事情,但这样做的缺点是每次更新这些方法,并且如果我们忘记了,可能会丢失功能。

当你需要深度复制时,你是否也需要将所有关联项进行深度复制?还是仅需要其中的一些? - Mike
如果你需要性能提升,手动实现clone()是一个不错的选择(我喜欢clone(),无论听到什么)。没有任何外部库可以明显超越Java的(反)序列化,因为它依赖于反射(在最好的情况下可能会选择代码生成,但仍然...)。 - bestsss
4个回答

1

您可以通过动态类生成(例如使用cglib)来避免反射。对于每个使用的类,您都需要生成一个“克隆器类”,其中包含所需的复制代码。只要所有字段至少是包私有的,并将复制器类放在同一包中,就可以避免使用反射。您还需要一个默认构造函数和没有final字段。

在这里,序列化具有优势,因为它使用sun.misc.Unsafe创建对象。

在每个类中实现deepClone方法也是一种选择。它甚至可以与克隆器类的想法结合起来。


字段访问使用反射非常快(特别是在setAccessible(true)之后 - 完全删除安全检查)。您甚至可以使用Unsafe.getXXX进行绝对内联。但是,经过第二次思考,原始类型的装箱分配成本仍然需要支付。 - bestsss
你可以使用 getType 然后使用 getXXX 和 setXXX 方法来处理原始类型,但是这仍然需要一些时间。你可以使用 Unsafe.copyMemory 来获取一个浅克隆,然后修复所有非原始类型字段。你需要知道对象的确切大小才能实现这一点。 - maaartinus

0

仅给你一个提示,告诉你如何在这种情况下提高性能:如果你还没有使用原型模式,请使用它。你可能会获得一些性能提升。


实际上,原型与编写副本方法没有什么区别,在我的情况下,有数百个类需要编写克隆方法,因为我们需要深度复制。无论是编写克隆还是复制方法,每次我们添加或删除这些类中的任何一个字段,都必须更改复制/克隆方法。 - K.M
如果您使用clone()添加基元或不可修改的类(URL、String、Numbers、URI等),则无需进行更改。编写克隆方法可能只需要一分钟。 - bestsss
不要浪费时间,现在考虑一个包含HashMap的类,其中包含另一种类型的对象(可能是基本类型,因此可能存在子类型),如果这些对象也包含集合。您必须为每个对象编写克隆函数以进行深度克隆,并确保在每个类中复制集合内容。Cloneable接口已经失效(Effective Java)。 - K.M
你需要两个实用函数来创建正确的Map/Collection类型,然后遍历它们并调用clone()。主要和唯一的问题是如果有循环引用,如何正确地访问图形。不会超过一分钟。老实说,我记不得写clone()花费了这么多时间。 - bestsss
啊,是的,我忘了你需要一个额外的接口来使clone()方法公开。 - bestsss
这意味着您正在说我们为每个类(100多个)提供克隆方法及其实现,并检查存储在哈希映射中的类,为每个类提供克隆()。每次我们向任何一个类添加或删除字段时,都需要更改clone()方法。有关克隆的更多信息,请参见Effective Java Item 10。链接/Chapter3.pdf - K.M

0

我怀疑按照你所提到的方法,性能有很大提升的空间。不幸的是,复制一个对象确实需要时间。

稍微有些侧重思考,并且显然取决于你拥有多少内存以及读写比例(特别是如果每次写入都有许多读取者),创建一份副本缓存如何?


谢谢您的回复。我也有同感,我正在尝试找出我们是否可以适当地重构它,这个区域需要适当的重构。 - K.M

0

你所描述的是一种管理可变状态的散弹式方法。试图使克隆更快只会给你带来有限的改进(比如说最多一个数量级)。此外,你如何克隆套接字、流或数据库连接?

你真正需要的是使用适当的命令/查询分离重构你的应用程序。这样你就会知道你在哪里改变了你的对象,以及你没有改变的地方。如果不确定,你可以使用单元测试来验证这一点。

有许多技术可以帮助你 - 你可以将一些对象更改为不可变的(这样你可以自然地共享它们,在突变时创建新副本);你可以使可变对象实现只读接口,并在 GUI 中使用 RO 视图等。


是的,我承认这是一种“散弹枪”方法,但我们必须在时间和功能之间做出权衡。这个项目非常重要,新功能必须在非常紧迫的期限内交付。 - K.M
是的,我承认这是一种“散弹枪”方法,但我们必须在时间和功能之间做出权衡。这个项目非常古老,新功能必须在非常紧的截止日期内交付。我们需要在不太改变代码的情况下获得一些性能提升,我想将克隆作为建议应用到我们的代码中,这会调用ObjectSerializer.createdCopy()(我们自己的代码)来进行深度复制。 - K.M

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