在机密数据库中有一些关于此错误 (JDK-5055732) 的重要信息。我已在有关该错误的公开评论中发布了此信息,我将在此处复制以回答此问题。
问题
正如 Josh Bloch 的 Effective Java 中所解释的,Cloneable 机制的设计并不是非常好。特别是,对于具有最终引用字段的非最终类来说,每个对象都必须是唯一的才能满足以下要求:
x.clone().getClass() == x.getClass()
(当类被子类化时)
目前指定CopyOnWriteArraySet、ConcurrentHashMap 来实现Cloneable。 CopyOnWriteArraySet 错误地没有实现 public clone() 方法,而 ConcurrentHashMap 使用构造函数实现了 clone() 方法,因此无法满足上述要求。
道格·李写道:
“Martin 和 Josh 说服了我,我们不能只添加一行 public Object clone() { return new CopyOnWriteArraySet(al); } 因为,正如 Josh 在 Effective Java book 中所指出的,克隆方法不应该调用构造函数:
实际上,程序员假设如果他们扩展一个类并从子类中调用 super.clone,则返回的对象将是子类的一个实例。超类可以提供此功能的唯一方法是返回通过调用 super.clone 获得的对象。如果一个克隆方法返回一个由普通构造函数创建的对象,那么它将没有正确的类。因此,如果在非 final 类中重写 clone 方法,则应始终返回通过调用 super.clone() 获得的对象。
通常这意味着任何具有空白 final 字段的类都会遇到问题,因为它需要在 clone 中设置字段。这现在可以在 JDK 类中使用 setAccessible 漏洞(参见 JMM 列表),但丑陋且缓慢。删除“implements Cloneable”似乎是一个更好的主意。
ConcurrentHashMap 类有完全相同的问题,同样的解决方案。”
解决方案
从 CopyOnWriteArraySet、ConcurrentHashMap 的规范中删除“实现 Cloneable”。删除 ConcurrentHashMap.clone()
上面的文字解释了一切,但它可能有点令人困惑,因为它解释了与不再可见的代码状态相关的事物,并且还假设了相当多的上下文知识。这是一个我认为可能更容易理解的解释。
Joshua Bloch 的 Effective Java 第 11 条对克隆问题进行了全面解释。elsewhere on Stack Overflow 也涵盖了许多问题。简而言之,要成功克隆,类必须
- 实现
Cloneable接口
- 实现
public clone() 方法
- 在
clone()方法中,必须
- 致电
super.clone() 进行实际克隆
- 修改克隆对象,可能通过深度复制内部结构
- 返回克隆的对象
从历史上看,所有集合实现都支持克隆。在 JDK 5.0 发布之前,CopyOnWriteArraySet 和ConcurrentHashMap 都实现了Cloneable 接口。但是CopyOnWriteArraySet 没有实现public clone() 方法,虽然ConcurrentHashMap 确实实现了public clone() 方法,但它通过返回一个新构造的ConcurrentHashMap 实例来错误地执行此操作。这两个都是错误,是本错误报告的主题。
事实证明,CopyOnWriteArraySet 和 ConcurrentHashMap 都无法履行支持克隆的所有义务。因此,该错误的“修复”是让他们退出Cloneable 合约。
CopyOnWriteArraySet 不能被克隆的原因是它有一个最终字段al 指向存储实际元素的CopyOnWriteArrayList。克隆不能与原始共享此状态,因此需要clone() 方法来复制(或克隆)后备列表并将其存储到字段中。但是 final 字段只能存储在构造函数中,clone() 不是构造函数。实现者考虑并拒绝了诸如使用反射编写 final 字段之类的英勇努力。
像这样的单行构造函数呢?
public clone() { return new CopyOnWriteArraySet(al); }
这里的问题是它违反了克隆合同。如果CopyOnWriteArraySet 的子类支持克隆,则在该子类上调用clone() 应返回该子类的实例。子类的clone() 方法将正确调用super.clone() 来创建克隆。如果按照上面的方式实现,那将返回CopyOnWriteArraySet 的实例,而不是子类的实例。因此,这将阻止子类能够克隆自己。
ConcurrentHashMap 呢?它没有任何最终字段。嗯,当时确实如此,所以它恰好遇到了从 clone() 方法中更新最终字段的问题。
ConcurrentHashMap 的最新版本不再具有最终字段。复制构造函数只是在 map 参数上调用 putAll,它会延迟初始化所有字段。难道clone()方法不能简单地通过克隆,清空所有字段,然后调用putAll()来实现吗?
这似乎可行,但我怀疑它与内存模型相冲突。并非所有字段都是易变的。即使在重新初始化以指向副本之前所有字段都被清空,其他线程可能会看到仍然指向原始映射的陈旧值。可能有办法避免这个问题,但我怀疑实现者认为提供可克隆性不值得付出额外的努力。