我想回答你最后一个问题的答案是“是”,只是为了好玩 :-) 但我知道你想要一些论据,所以让我试试:
如果一个概念(这里是复制)是唯一的对象,则可以考虑在同一类中合并两个概念(这将在类本身中实现该方法)。例如:
- 如果对于某些对象,有几种复制方式,取决于......,那么复制确实应该脱离原始对象。
- 如果对于所有对象,都有一种唯一的复制方式,那么创建一个不同的对象来执行复制是否真的有很大价值?(例如,如果总复杂度太高,难以阅读和理解,因此最好将其分解)。
我一直很难通过调用显式构造函数来创建副本。原因是构造函数是唯一无法继承的方法(除了静态方法...),因此它们不能是通用的(不可能为所有可复制的对象拥有唯一的接口)。这意味着您无法在整个应用程序中编写通用代码,以复制您的对象。每次我尝试时,都会有一个时刻,我真的需要以一般方式进行复制。
显式调用构造函数也意味着将来将无法替换子类。假设您有一个算法A,它在变量B上工作。如果将B的子类C传递给A,则当A创建其B变量的副本(其实际类型为C)时,将使用B构造函数创建该副本,因此它将不是相同的类,并且可能会更改行为。因此,通过调用构造函数进行复制非常有限。
显式调用构造函数意味着无法使用接口。您可以在许多地方阅读有关接口的价值......因此,例如,在我们的应用程序中,许多对象不是直接在我们的代码中实例化的,而是要求定位器/工厂提供接口(或类),具有许多可能的优势(如果您的应用程序某天需要这个):
- 如果我想在特定上下文中将每个A对象替换为B子类,例如在某些自动化测试期间测量昂贵操作的性能,那么这很容易。我们还需要将HashMaps替换为子类,以查找插入到Map中的一个非可序列化对象,后来在序列化期间导致错误。
- 如果有一个接口,那么在我的常规代码中,只涉及接口创建对象(除了工厂)。因此,我根本没有依赖于具体类,这是非常好的,因为您知道(对接口的依赖关系具有更少的传递依赖关系,并且更容易进行测试模拟)。
- 在我们的情况下,这个工厂实际上是由Spring支持的,因此通过Spring完成实例化。需要采取许多其他步骤(代理,拦截,初始化方法......)。
在我们的应用程序中,通常会创建一个(或几个)克隆器。给定一个顶级对象,它们知道如何制作它的深层副本。使用通用克隆器的优点是代码只需编写一次,适用于整个应用程序。通常,它还在应用程序之间重复使用...
实现:例如,使用反射递归获取每个成员。然而有许多要避免的陷阱:
- 循环:A引用B,B引用C,C引用A。因此,我保留了已经被复制的对象的Map,引用该复制品。当我复制一个对象,但发现它已经在映射中时,我不会复制它,而是替换其已经制作好的复制品。
- 特殊类型:枚举不应该被复制(还有其他一些静态对象)。某些库类也可能存在问题,因此您可以保留不希望复制或以特殊方式复制的特殊类的Set或Map。
- 您可能会遇到final字段的问题...
特定情况
通常有特定的对象,其中默认方式不正确。我们既希望有一个通用实现,又希望根据需要进行重载。对于它们,我们使用以下方法:
- 如果我们可以修改对象,则让它们实现特定的CustomizedCopier接口,并且它们在该方法中的代码负责按照他们想要的方式进行复制。如果通用代码看到这个接口,它将不执行任何操作。
- 如果我们无法修改对象(JRE,第三方代码...),则有一个Map/Registry Map存储特定类以及我们希望为它们使用的特定复制器。请注意,有时也会使用此技巧来自定义复制,而不是一般性地自定义复制,仅适用于某些特殊用例,因为它可以重载对象复制的方式。
实际上,我通常会最终使用几个克隆器。例如,克隆数据持久化实体通常会使用此知识以稍微不同的方式进行克隆(例如,ID和审核字段可以设置为null)。
我通常还有一个类,用于满足其他需求的依赖项搜索:
- toString():创建一个复杂对象的调试字符串。
- equals()和hashCode()实现(如果需要)。
- 重新初始化对象图形的所有属性的默认值(考虑多选项卡巨大表单中“重置”按钮的实现)。
- 检查对象图中某个位置的对象是否存在。
- 控制对象图的可序列化性(HttpSession是序列化的典型用例;在开发过程中,我们显式检查对象,以检测不可序列化的对象,并为开发人员提供最佳错误消息)。
- ...
请注意,复制通常用于多线程。理想情况下,在多线程环境中重用的对象是不可变的。如果不是,则通常建议进行克隆以确保程序全局一致性...
性能
使用反射并不总是很快。通常,对于大量使用且经常需要复制的复制,我们会在对象本身中实现复制。但是我们发现只有少数几个需要被复制且数量庞大的类,因此这只是一种例外情况,我们会在它们变得有用时(我之前在帖子中介绍了如何使用注册表)插入它们。