为什么Cloneable接口没有被弃用?

146
众所周知,Java 中的 Cloneable 接口存在缺陷。这种情况有很多原因,我就不一一列举了;其他人已经做过了。链接。这也是Java 架构师们的立场。
因此,我的问题是:为什么它还没有被弃用呢?如果核心 Java 团队已经认定它存在缺陷,那么他们肯定也考虑过废除它。他们不这样做的原因是什么(在 Java 8 中,它仍未被弃用)?

42
许多人似乎认为这个问题是“主观性强”,但事实并非如此。那些只有观点而没有更多理由的人不够资格回答。然而,在这里得到权威答案的机会很小。同时,你的问题也不是一个可以解决的问题,所以至少可以说边缘话题。 - Marko Topolnik
6
我同意@MarkoTopolnik所说,世界上有些人可以提供权威的答案,但我不认为这是我们应用的测试。关闭原因说明“对这个问题的回答往往几乎完全基于意见”。我怀疑这将是此处的情况,除非我们非常幸运。 - Duncan Jones
8
@Duncan,我仍然认为根据我对回答者缺乏纪律性的假设来对问题进行评判是不公平的。如果用户不知道被询问的原因,他/她就没有资格滥用回答功能来表达自己对此事的看法。 - Marko Topolnik
4
是的,正是如此——你可以打赌他们已经彻底地考虑了这个选项,并且暗示着他们一定有强有力的理由不去弃用它。他们自己对Cloneable的批评是广为人知的。 - Marko Topolnik
2
@OlegEstekhin,“Cloneable”是有问题的,这不是一种观点,而是一个医学事实。 - lexicore
显示剩余28条评论
3个回答

123

1997年有人向Java Bug Database提交了一个关于在Cloneable中添加clone()方法的bug,以使其不再无用。它被关闭并标记为“不修复”,原因如下:

Sun的技术审查委员会(TRC)详细考虑了这个问题,并建议不采取任何行动,除了改进当前Cloneable接口的文档。以下是建议的完整文本:
现有的Java对象克隆API存在问题。java.lang.Object上有一个受保护的“clone”方法,还有一个接口java.lang.Cloneable。意图是,如果一个类想要允许其他人克隆它,那么它应该支持Cloneable接口,并用公共克隆方法覆盖默认受保护的克隆方法。不幸的是,由于方便地丢失在时间的迷雾中,Cloneable接口没有定义克隆方法。
这种组合导致相当多的混淆。一些类声称支持Cloneable,但不小心忘记支持克隆方法。开发人员对Cloneable应该如何工作以及克隆应该做什么感到困惑。
不幸的是,向Cloneable添加“克隆”方法将是一种不兼容的更改。它不会破坏二进制兼容性,但它会破坏源代码兼容性。轶闻证据表明,在实践中有许多情况下,类支持Cloneable接口,但未提供公共克隆方法。经过讨论,TRC一致建议我们不应修改现有的Cloneable接口,因为这会影响兼容性。
另一个提案是添加一个新接口java.lang.PubliclyCloneable,以反映Cloneable的最初目的。经过5比2的多数票,TRC建议不采用此方案。主要关注点是这将增加更多混淆(包括拼写混淆!)到已经混乱的画面中。
TRC一致建议我们应向现有的Cloneable接口添加附加文档,以更好地描述它的使用方式并描述实现者的“最佳实践”。
因此,虽然这与“不推荐使用”无关,但技术审查委员会决定修改现有文档足以使克隆接口有用。他们也这么做了。直到Java 1.4,Cloneable的文档如下所示:
一个类实现Cloneable接口,以指示Object.clone()方法可以合法地对该类的实例进行逐个字段的复制。尝试克隆未实现Cloneable接口的实例将导致抛出CloneNotSupportedException异常。Cloneable接口没有声明任何方法。
自Java 1.4(发布于2002年2月)至当前版本(Java 8),它看起来像这样:
一个类实现Cloneable接口,表示该类的实例可以被Object.clone()方法逐个字段地复制。在没有实现Cloneable接口的实例上调用Object的clone方法会导致抛出CloneNotSupportedException异常。
按照惯例,实现此接口的类应该使用公共方法覆盖Object.clone(受保护)。有关覆盖此方法的详细信息,请参阅Object.clone()。
请注意,此接口不包含clone方法。因此,仅凭它实现了这个接口并不能克隆一个对象。即使通过反射调用clone方法,也不能保证成功。

3
你知道为什么clone方法最初会在Object中吗? - njzk2
8
这是机制的重要组成部分——它是执行低级别、超语言幻术以逐位复制对象图像的方法。 - Marko Topolnik
3
那种魔法是无法通过复制构造函数来复制的:Object#clone() 方法会产生一个与原始实例相同类的新实例,但该类在编译时是未知的。 - Marko Topolnik
4
这句话的意思是:“@AVolpe说,这并不能解决答案中提到的源代码不兼容问题,即类支持Cloneable接口但未提供公共的clone方法。特别地,提供非公共的clone方法的类仍然会出现问题。” - Louis Wasserman
2
不错的历史记录。这里是一个直接链接到错误数据库的链接。我在那里添加了一些更多的历史记录,并在我的答案中引用了其中的部分。 - Stuart Marks
显示剩余7条评论

66
"为什么Cloneable没有被弃用?"(或者为什么任何X都没有被弃用?)的简短回答是,很少有人关注废弃它们。最近被废弃的大多数东西之所以被废弃,是因为有一个具体的计划要将它们移除。例如,LogManageraddPropertyChangeListenerremovePropertyChangeListener方法在Java SE 8中已经被废弃,并计划在Java SE 9中将其移除。(原因是它们不必要地复杂了模块间的依赖关系。)事实上,这些API已经从早期的JDK 9开发构建中被移除。(请注意,类似的属性更改监听器调用也已从Pack200中删除;请参见JDK-8029806。)"
没有类似的计划来处理Cloneable和Object.clone()。
更长的答案将涉及进一步讨论,例如人们可能期望这些API发生什么变化,如果它们被弃用,平台会产生哪些成本或收益,以及当API被弃用时向开发人员传达了什么信息。我在最近的JavaOne演讲中探讨了这个话题,债务和弃用。(幻灯片可在该链接处获得; 视频在此。)结果证明,JDK本身在使用弃用方面并不一致。它被用来表示几种不同的含义,包括例如,
  • 这很危险,你应该意识到使用它的风险(例如:Thread.stop()Thread.resume()Thread.suspend())。

  • 这将在未来的版本中移除。

  • 这已经过时了,最好使用其他东西(例如:java.util.Date中的许多方法)。

所有这些都是不同的含义,并且不同的子集适用于被弃用的不同事物。其中某些子集适用于未被弃用的事物(但也许应该被弃用)。

CloneableObject.clone()在设计上存在缺陷,难以正确使用,因此可以说是“有问题的”。然而,clone()仍然是复制数组的最佳方式,并且克隆在一定程度上有用,可以复制小心实现的类的实例。删除克隆将是一项不兼容的更改,会破坏很多东西。可以用不同的方式重新实现克隆操作,但它可能比Object.clone()慢。

然而,对于大多数事情而言,拷贝构造函数比克隆更可取。因此,将Cloneable标记为“已过时”或“已被取代”或类似的内容可能是合适的。这会告诉开发人员他们可能需要寻找其他替代方案,但不会表明克隆机制可能会在未来的版本中删除。不幸的是,没有这样的标记存在。

目前的情况是,“弃用”似乎意味着最终会被删除--尽管极少量的弃用功能曾经被删除--因此,克隆机制似乎不应该被弃用。也许在将来可以应用另一种标记,指导开发人员使用替代机制。

更新

我在错误报告中添加了一些额外的历史。Frank Yellin是早期JVM实现者之一,也是JVM规范的合著者,在其他答案中引用了TRC建议中的“遗失在时间的雾中”的评论,并对其进行了一些评论。以下是相关部分的引用;完整信息请参阅错误报告。

Cloneable没有方法的原因和Serializable一样。Cloneable表示类的属性,而不是具体支持哪些方法。
在反射出现之前,我们需要一个本地方法来创建一个对象的浅拷贝。因此Object.clone()应运而生。很明显许多类都想要重写这个方法,而且并不是每个类都想被克隆。于是Cloneable诞生了,用来表示程序员的意图。
简而言之,Cloneable的目的不是为了表明你有一个公共的clone()方法,而是表明你愿意使用Object.clone()进行克隆,具体是否将clone()设置为公共方法由实现决定。

3
您的回答很好,先生。我特别喜欢您不仅仅因为其他人想要而随意使用Object.clone(),而是愿意进行推理并提出其好处。 - icza
2
然而,clone() 仍然是复制数组的最佳方法,并且克隆在某些情况下对于制定实现的类的实例复制具有有限的用处。我曾经认为通过修复6428387,所有代码路径(clone、new/arrayCopy、Arrays.copyOf)都会产生相同的内部操作。最近有什么变化吗? - bestsss
2
@bestsss,我认为array.clone()并不一定比其他任何替代方法更快。从API的角度来看,这是复制数组最简洁的方式。Arrays.copyOf(array, newlen)接近于它,但需要一个长度参数,如果您不改变长度,则该参数是多余的。 - Stuart Marks
2
@Holger 是的,就我们所知,这是自1.1以来第一次实际删除API。还要注意的是,即使我们同意Thread.suspend()Thread.stop()(无参)是危险的,它们可能不会被删除--或者改为无条件抛出异常--因为人们实际上在使用它们!大概他们愿意承担风险。属性更改侦听器的缓解因素之一是它们很少被使用,因此删除它们的影响很小。 - Stuart Marks
2
从概念上讲,java.beans 库API只是一个属性的库,因此可以使其与 java.desktop 无关。可惜的是,如果深入研究 beans API 就会发现有很多对 AWT 的依赖。实现甚至更多。当然,可能有可能将它们分离出来,但做到这一点似乎比例如将记录与bean解脱出来要困难得多。整个模块化工作的目的就在于进行这种解脱操作。毫无疑问,还可以做更多的工作,但那样做会使 Jigsaw 花费更长时间。 - Stuart Marks
显示剩余8条评论

-2
为什么它还没有被弃用?
因为JCP认为不需要这样做,而且可能永远不会这样做。请向他们询问。你在错误的地方提问了。
保留Java API中这个东西的原因是什么?
由于向后兼容性要求,没有人会从Java API中删除任何内容。上一次发生这种情况是1996/7年1.0和1.1之间的AWT事件模型更改。

17
他们通过更改 Thread.stop(Throwable) 的契约以始终向调用者(而不是目标线程)抛出 UnsupportedOperationException 来有效地删除了它。请注意,这并不意味着它已从语言中完全消失。 - Marko Topolnik
22
大约在同一时间发生了什么事情?Java 8 中移除了 Thread.stop(Throwable) 的功能。无论如何,“去问他们”的不合格建议是错误的,因为如今首席 Java 架构师本人是 Stack Overflow 上的活跃成员。他只是碰巧只回答与 Streams 相关的问题而已。 - Marko Topolnik
13
另外,OP的问题不是关于“移除”,而是关于“弃用”,而很明显弃用一直在发生。 - Marko Topolnik
18
@EJP,我不是在问Cloneable是否会从Java API中删除。我在问为什么它不会被弃用。我认为这是一个很好的询问地点。 - Kao
5
谢谢你的提及,但我不是Java架构师。我是Oracle JDK组的工程师,负责维护这些东西。 - Stuart Marks
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接