为什么CopyOnWriteArraySet不实现Cloneable接口,而CopyOnWriteArrayList呢?

时间:2017-07-20 11:01:57

标签: java collections clone cloneable

在这篇bug report中,Doug Lea写道(指的是JDK 5.0的预发布版本):

  

虽然CopyOnWriteArraySet被声明为Cloneable,但它无法定义公共克隆方法。

但它最终导致CopyOnWriteArraySet根本没有实现Cloneable接口! (在Java SE 6,7和8中都是如此。)

CopyOnWriteArraySet与克隆有什么不同CopyOnWriteArrayList?有没有人想要克隆它?

P.S。我了解不建议使用clone()CopyOnWriteArraySet内部基于CopyOnWriteArrayList

1 个答案:

答案 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发布之前,CopyOnWriteArraySetConcurrentHashMap都实现了Cloneable接口。但是CopyOnWriteArraySet没有实现public clone()方法,而ConcurrentHashMap确实实现了public clone()方法,它通过返回一个新构造的{{ {1}}。这两个都是错误,并且是此错误报告的主题。

事实证明,ConcurrentHashMapCopyOnWriteArraySet都不能履行支持克隆的所有义务。 "修复"那么,对于这个错误,就是让他们退出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()方法吗?

这似乎可能有用,但我怀疑它与内存模型相冲突。并非所有字段都是易变的。即使在重新初始化以指向副本之前所有字段都被清空,其他线程也可能会看到仍然指向原始地图的陈旧值。可能有办法避免这个问题,但我怀疑实现者认为提供可克隆性并不值得付出额外的努力。