tags:

views:

281

answers:

6

This bug took me a while to find...

Consider this method:

public void foo(Set<Object> set)
{
    Object obj=set.iterator().next();
    set.remove(obj)
}

I invoke the method with a non-empty hash set, but no element will be removed!

Why would that be?

+2  A: 

Puzzle? If Object.hashCode, Object.equals or the "hash set" were implemented incorrectly (see for instance, java.net.URL - use URI).

Also if the set (directly or indirectly) contains itself, something odd is likely to happen (exactly what is implementation and phase of the moon dependent).

Tom Hawtin - tackline
Hi Tom - the object being returned by the iterator is the same instance that is passed to Set.remove(Object), so the equals would have to be pretty messed up to be the cause the remove to fail. Likewise the set containing reference to itself, would be a little bit messed up but there's no recursion going on here so it should still work (although it doesn't with HashSet dues to a stack-overflow creating the iterator - looks like there's some recursion in the implementation). I reckon you're on the right track with the implementation of Set - Nick
Nick Holt
Just read @Adrian Pronk's answer about the hash code changing after the object is added - guess that would still count as messed-up...
Nick Holt
There are lots of ways to implement methods that don't obey their spec! You could actually have a valid implementation, but change state from another thread.
Tom Hawtin - tackline
(Btw: Whoever put a downvote on this: Would you like to explain?)
Tom Hawtin - tackline
Hi Tom - that was me. At the time your answer had more up votes than Adrian's answer, which hadn't yet been accepted, I'd tested and found to be correct. I also felt that the reference to hashCode and equals were vague, the later (equals) as I pointed out in my previous comment had, I felt, missed that it was the same object was being passed to the remove method and while I agree multiple threads could cause this, there was no mention of threads in the question or answer - hence I felt it warranted a down vote, obviously nothing personal I trust you understand - cheers Nick
Nick Holt
+1  A: 

Beyond the missing ';' after set.remove(obj), It can happen in three situations (quoted from javadoc).

ClassCastException - if the type of the specified element is incompatible with this set (optional).
NullPointerException - if the specified element is null and this set does not support null elements (optional). 
UnsupportedOperationException - if the remove method is not supported by this set.

You can also try:

public void foo(Set<Object> set)
{
    Object obj=set.iterator().next();
    iterator.remove();
}
freitass
This is what you have to do. The collection classes does not guarantee what happens if you remove something while iterating other than calling the iterator.remove() method(Which might throw an "UnsupportedOperatrion" for some collection classes.
nos
+1  A: 

Should it be:

public void foo(Set<Object> set)
{
    Iterator i = set.iterator();
    i.next();
    i.remove();
}

?

The bug could be something to do with:

public void remove()

The behavior of an iterator is unspecified if the underlying collection is modified while the iteration is in progress in any way other than by calling this method.

(Reference)

Blorgbeard
That would be a better way of doing it (don't know why you would want to remove a randomish element from a set though). _HashSet.remove_ is okay in the original code because the iterator is no longer being used.
Tom Hawtin - tackline
+2  A: 

What is the implementation type of the set and what objects are inside the set?

  • If it is a HashSet, make sure that the value of the object's hashCode() method remains constant between set.put(...) and set.remove(...).
  • If it is a TreeSet, make sure that not modifications were made to the object that affect the set's comparator or the object's compareTo method.

In both cases, the code between set.put(...) and set.remove(...) violates the contract defined by the respective class implementation. As a rule of thumb, it is a good idea to use immutable objects as set content (and as Map keys). By their very nature such objects cannot be changed while they are stored inside a set.

If you are using some other set implementation, check out its JavaDoc for its contract; but usually either equals or hashCode must remain the same while the object is contained in the set.

nd
+2  A: 

For a HashSet, this can occur if the object's hashCode changes after it has been added to the set. The HashSet.remove() method may then look in the wrong Hash bucket and fail to find it.

This probably wouldn't happen if you did iterator.remove(), but in any case, storing objects in a HashSet whose hashCode can change is an accident waiting to happen (as you've discovered).

Adrian Pronk
That sounds spot on - those auto-gen'd hashCode methods that use all the fields of a class, some of which are mutable would be a likely candidate to cause this.
Nick Holt
Just tried to use remove with an object that has had its hash code changed after it was added and the remove does indeed fail. Incidentally so does iterator.remove().
Nick Holt
That was the problem, indeed.Fixed by using a List instead (there were never more than a few elements in the set anyway, so no big performance penalty here).
Yvon Rozijn
A: 

I can't help but feel that (part of) the problem is that the set is passed by value, not reference. I don't have much experience in Java though, so I could be totally wrong.

Matthew Iselin
You're right: you're totally wrong :)
Adrian Pronk
Right, objects can never be passed by value in Java. (Primitives are.)
Sean Owen
@Sean Owen - everything is passed by value, in the case of an object it's the value of the reference (pointer to the object) that is passed.
Nick Holt
Nick - passing a value that is a reference/pointer is passing by reference, otherwise what can passing by reference mean?
CiscoIPPhone