【问题标题】:Why doesn't CopyOnWriteArraySet implement the Cloneable interface, while CopyOnWriteArrayList does?为什么 CopyOnWriteArraySet 不实现 Cloneable 接口,而 CopyOnWriteArrayList 实现?
【发布时间】:2017-12-26 00:57:06
【问题描述】:

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

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

但最终CopyOnWriteArraySet 根本没有实现Cloneable 接口! (在 Java SE 6、7 和 8 中确实如此。)

CopyOnWriteArraySetCopyOnWriteArrayList 在克隆方面有何不同?没人想克隆它有充分的理由吗?

附:我了解不推荐使用clone(),并且CopyOnWriteArraySet 内部基于CopyOnWriteArrayList

【问题讨论】:

  • “为什么CopyOnWriteArraySet 不实现Clingoaneable 接口,而CopyOnWriteArraySet 实现?” 咦?!?你说CopyOnWriteArraySet 同时做和不做。或者你的意思是为其中一个写一些其他的类名?
  • 基本上,您向我们询问我们没有人参与的软件开发的原因 - 以及多年前的代码。这是一个关于软件历史的问题,而不是关于编程的问题。
  • 反对者和接近者:请保持这个问题的开放性。我有这个问题的答案,但需要一点时间才能挖掘出来。

标签: java collections clone cloneable


【解决方案1】:

在机密数据库中有一些关于此错误 (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 发布之前,CopyOnWriteArraySetConcurrentHashMap 都实现了Cloneable 接口。但是CopyOnWriteArraySet 没有实现public clone() 方法,虽然ConcurrentHashMap 确实实现了public clone() 方法,但它通过返回一个新构造的ConcurrentHashMap 实例来错误地执行此操作。这两个都是错误,是本错误报告的主题。

事实证明,CopyOnWriteArraySetConcurrentHashMap 都无法履行支持克隆的所有义务。因此,该错误的“修复”是让他们退出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()来实现吗?

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

【讨论】:

    猜你喜欢
    • 2015-11-22
    • 1970-01-01
    • 2021-06-29
    • 1970-01-01
    • 2016-06-20
    • 2020-03-26
    • 1970-01-01
    • 2012-08-29
    • 2012-01-28
    相关资源
    最近更新 更多