Why doesn't CopyOnWriteArraySet implement the Cloneable interface, while CopyOnWriteArrayList does?

寵の児 提交于 2020-01-01 07:11:08

问题


In this bug report, Doug Lea writes (referring to a pre-release version of JDK 5.0):

While CopyOnWriteArraySet is declared Cloneable, it fails to define public clone method.

But it eventually ends up that CopyOnWriteArraySet doesn't implement the Cloneable interface at all! (This is true in Java SE 6, 7, and 8.)

How is CopyOnWriteArraySet different from CopyOnWriteArrayList with respect to cloning? There is a sound reason nobody ever wants to clone it?

P.S. I understand that clone() is not recommended and that CopyOnWriteArraySet is based on CopyOnWriteArrayList internally.


回答1:


There was some important information about this bug (JDK-5055732) in a confidential database. I've posted this information in a public comment on that bug, and I'll copy it here to answer this question.

Problem

As explained in Josh Bloch's Effective Java, the Cloneable mechanism is not terribly well designed. In particular, it is impossible for a non-final class with a final reference field that needs to be unique for each object to satisfy the requirement that

x.clone().getClass() == x.getClass()

(when the class is subclassed)

CopyOnWriteArraySet, ConcurrentHashMap currently are specified to implement Cloneable. CopyOnWriteArraySet erroneously did not implement a public clone() method, while ConcurrentHashMap implemented a clone() method using a constructor, thereby not fulfilling the above requirement.

Doug Lea writes:

"Martin and Josh convinced me that we can't just add the one-line public Object clone() { return new CopyOnWriteArraySet(al); } because, as noted by Josh in Effective Java book, clone methods should not invoke constructors:

In practice, programmers assume that if they extend a class and call super.clone from within the subclass, the returned object will be an instance of the subclass. The only way that a superclass can provide this functionality is to return an object obtained by calling super.clone. If a clone method returns an object created by a normal constructor, it will not have the correct class. Therefore, if you override the clone method in a non-final class, you should always return an object obtained by invoking super.clone().

In general this means that any class with a blank final field will encounter problems because it needs to set the field inside clone. This is now possible inside JDK classes using the setAccessible loophole (see JMM list) but is ugly and slow. It seems like a better idea just to remove "implements Cloneable".

The ConcurrentHashMap class has exactly the same problem, and the same solution."

Solution

Remove "implements Cloneable" from the specification for CopyOnWriteArraySet, ConcurrentHashMap. Delete ConcurrentHashMap.clone()

The text above explains everything, but it might be a bit confusing since it explains things relative to the state of the code that's no longer visible, and it also assumes a fair amount of contextual knowledge. Here's an explanation that I think might be a bit easier to understand.

The issues with cloning are fully explained in Joshua Bloch's Effective Java, Item 11. Much of the issues are also covered elsewhere on Stack Overflow. Briefly, to allow successful cloning, a class must

  • implement the Cloneable interface
  • implement a public clone() method
  • in the clone() method, it must
    • call super.clone() to do the actual cloning
    • modify the cloned object, possibly by deep-copying internal structures
    • return the cloned object

Historically, all collections implementations have supported cloning. Prior to the release of JDK 5.0, CopyOnWriteArraySet and ConcurrentHashMap both implemented the Cloneable interface. But CopyOnWriteArraySet didn't implement a public clone() method, and while ConcurrentHashMap did implement a public clone() method, it did so incorrectly, by returning a freshly constructed instance of ConcurrentHashMap. Both of these are bugs and are the subject of this bug report.

It turns out that neither CopyOnWriteArraySet nor ConcurrentHashMap can fulfill all the obligations of supporting cloning. The "fix" for the bug, then, was to have them withdraw from the Cloneable contract.

The reason CopyOnWriteArraySet can't be cloned is that it has a final field al that points to the CopyOnWriteArrayList that stores the actual elements. The clone mustn't share this state with the original, so the clone() method would be required to copy (or clone) the backing list and store it into the field. But final fields can only be stored within constructors, and clone() isn't a constructor. The implementors considered and rejected heroic efforts such as using reflection to write final fields.

What about a one-liner constructor such as this?

    public clone() { return new CopyOnWriteArraySet(al); }

The problem here is that it breaks the cloning contract. If a subclass of CopyOnWriteArraySet were to support cloning, calling clone() on that subclass should return an instance of that subclass. The clone() method of the subclass would properly call super.clone() to create the clone. If it were implemented as above, that would return an instance of CopyOnWriteArraySet instead of an instance of the subclass. This would therefore prevent subclasses from being able to clone themselves.

What about ConcurrentHashMap? It doesn't have any final fields. Well, it did at the time, so it suffered from exactly the problem of updating final fields from within the clone() method.

Recent versions of ConcurrentHashMap no longer have final fields. The copy constructor simply calls putAll on the map argument, which initializes all the fields lazily. Couldn't the clone() method be implemented simply by cloning, nulling out all the fields, and then calling putAll()?

This seems like it might work, but I suspect it runs afoul of the memory model. Not all of the fields are volatile. Even if all fields were nulled out before being reinitialized to point to copies, other threads might see stale values that still pointed to the original map. There might be ways to avoid this problem, but I suspect the implementors felt that providing cloneability wasn't worth the extra effort.



来源:https://stackoverflow.com/questions/45212827/why-doesnt-copyonwritearrayset-implement-the-cloneable-interface-while-copyonw

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!