'ClassCastException in HashSet.put()

In seemingly simple code - as far as it relates to the problem - such as this:

private final HashSet<ValueType> set = new HashSet<>();
...
void doIt(ByteBuffer b) {
  set.add(new ValueType(b));
}

I got this ClassCastException in prod, out of the blue:

java.lang.ClassCastException: class java.util.HashMap$Node cannot be cast to class java.util.HashMap$TreeNode (java.util.HashMap$Node and java.util.HashMap$TreeNode are in module java.base of loader 'bootstrap')

        at java.base/java.util.HashMap$TreeNode.moveRootToFront(HashMap.java:1882) ~[na:na]
        at java.base/java.util.HashMap$TreeNode.treeify(HashMap.java:1998) ~[na:na]
        at java.base/java.util.HashMap.treeifyBin(HashMap.java:767) ~[na:na]
        at java.base/java.util.HashMap.putVal(HashMap.java:639) ~[na:na]
        at java.base/java.util.HashMap.put(HashMap.java:607) ~[na:na]
        at java.base/java.util.HashSet.add(HashSet.java:220) ~[na:na]
...

The Java VM used is Corretto-11.0.3.7.1 (11.0.3+7-LTS) on Linux.

I wonder if that is a bug in HashSet/HashMap?

The only other possibility I can imagine is that it is due to me adding elements to the hash set from different threads, which I don't think I'm doing, but I will verify now. am not doing. All calls to doIt() are inside x.lock() or if (x.tryLock()) blocks, where x is a final ReentrantLock. So yes, multi-threaded, but properly synchronized.

Although I definitely agree with user207421 and Stephen C in the comments that it would be unlikely for such a bug to still be in HashMap...

Edit After having cleared the multi-threading question, my focus is now on the ValueType. It is supposed not to change, but it is theoretically possible. Will need to verify if it can indeed get changed in practice. The following snippet shows that class, stripped of all irrelevant details. So b could potentially get changed outside of ValueType, after having passed into its constructor, which would be bad, as it would affect hashCode() and equals.

public class ValueType {
    private final ByteBuffer b;
    private int hash;

    ValueType(ByteBuffer b) {
        this.b = b.duplicate();
        this.b.clear();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || ValueType.class != o.getClass()) return false;

        ValueType that = (ValueType) o;
        return b.equals(that.b);
    }

    @Override
    public int hashCode() {
        int h = hash;
        if (h == 0) {
            final int pos = b.position(); // will actually always be 0
            for (int i = b.limit() - 1; i >= pos; i--)
                h = 31 * h + b.get(i);
            hash = h;
        }
        return h;
    }
}


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source