在这篇bug report中,Doug Lea写道(指的是JDK 5.0的预发布版本):
虽然
CopyOnWriteArraySet
被声明为Cloneable
,但它无法定义公共克隆方法。
但它最终导致CopyOnWriteArraySet
根本没有实现Cloneable
接口! (在Java SE 6,7和8中都是如此。)
CopyOnWriteArraySet
与克隆有什么不同CopyOnWriteArrayList
?有没有人想要克隆它?
P.S。我了解不建议使用clone()
,CopyOnWriteArraySet
内部基于CopyOnWriteArrayList
。
答案 0 :(得分:5)
在机密数据库中有关于此错误(JDK-5055732)的一些重要信息。我已在此漏洞的公开评论中发布此信息,我将其复制到此处以回答此问题。
问题
正如Josh Bloch的Effective Java中所解释的那样,Cloneable机制设计得并不是很好。特别是,对于每个对象而言,具有最终引用字段的非final类不可能是唯一的,以满足
的要求x.clone().getClass() == x.getClass()
(当类被子类化时)
CopyOnWriteArraySet,当前指定ConcurrentHashMap来实现Cloneable。 CopyOnWriteArraySet错误地没有实现公共clone()方法,而ConcurrentHashMap使用构造函数实现了clone()方法,因此无法满足上述要求。
Doug Lea写道:" Martin和Josh说服我,我们不能只添加单行公共Object clone(){return new CopyOnWriteArraySet(al);因为正如Josh在Effective Java一书中所指出的,克隆方法不应该调用构造函数:
实际上,程序员假设如果他们扩展一个类并从子类中调用super.clone,则返回的对象将是子类的实例。超类可以提供此功能的唯一方法是返回通过调用super.clone获得的对象。如果clone方法返回由普通构造函数创建的对象,则它将没有正确的类。因此,如果在非final类中重写clone方法,则应始终返回通过调用super.clone()获得的对象。
一般来说,这意味着任何具有空白最终字段的类都会遇到问题,因为它需要在克隆中设置字段。现在可以在JDK类中使用setAccessible漏洞(参见JMM列表),但这很丑陋。删除"实现Cloneable"。
似乎是一个更好的主意ConcurrentHashMap类具有完全相同的问题和相同的解决方案。"
解决方案
删除"实现Cloneable"来自CopyOnWriteArraySet,ConcurrentHashMap的规范。删除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()
方法,它通过返回一个新构造的{{ {1}}。这两个都是错误,并且是此错误报告的主题。
事实证明,ConcurrentHashMap
和CopyOnWriteArraySet
都不能履行支持克隆的所有义务。 "修复"那么,对于这个错误,就是让他们退出ConcurrentHashMap
合同。
无法克隆Cloneable
的原因是它有一个最终字段CopyOnWriteArraySet
,指向存储实际元素的al
。克隆不能与原始状态共享此状态,因此需要CopyOnWriteArrayList
方法复制(或克隆)支持列表并将其存储到字段中。但是最终字段只能存储在构造函数中,而clone()
不是构造函数。实现者考虑并拒绝了英雄的努力,例如使用反射来编写最终字段。
像这样的单线构造器怎么样?
clone()
这里的问题是它打破了克隆合同。如果 public clone() { return new CopyOnWriteArraySet(al); }
的子类支持克隆,则在该子类上调用CopyOnWriteArraySet
应该返回该子类的实例。子类的clone()
方法将正确调用clone()
来创建克隆。如果它是如上所述实现的,那将返回super.clone()
的实例而不是子类的实例。因此,这将阻止子类克隆自己。
CopyOnWriteArraySet
怎么样?它没有任何最终字段。嗯,它当时做了,所以它完全遇到了从ConcurrentHashMap
方法更新最终字段的问题。
clone()
的最新版本不再包含最终字段。复制构造函数只是在map参数上调用ConcurrentHashMap
,它会懒惰地初始化所有字段。无法通过克隆,清空所有字段,然后调用putAll
来实现clone()
方法吗?
这似乎可能有用,但我怀疑它与内存模型相冲突。并非所有字段都是易变的。即使在重新初始化以指向副本之前所有字段都被清空,其他线程也可能会看到仍然指向原始地图的陈旧值。可能有办法避免这个问题,但我怀疑实现者认为提供可克隆性并不值得付出额外的努力。