Wednesday, April 9, 2008

Removing an Entry from a Java Map During Iteration

I am a big fan of the enhanced Java for-each loop that has been available since J2SE 5. The vast majority of the iterations I need to do over collections fit the appropriate uses of this enhanced for loop and it has become a habit to use it now for all Collections iterating.

If I ever forget that there are cases where the enhanced for loop does not apply, I am quickly reminded when I use it in such cases. The three cases in which the enhanced for loop is not allowable are in iterating over parallel collections, in trying to change values in the collection as you iterate, and in removing elements from the collection you are iterating. The last usage is the subject of this blog entry.

The Collections Framework documentation makes it very clear that items can only be safely removed from an iterated collection via the Iterator's remove method. Because the enhanced for loop hides the Iterator (making the code so much cleaner and more concise), removal is not an option with this loop style.

The following code snippet demonstrates how to safely remove an element from a Java Map and also demonstrates how NOT to do this with commented out code that leads to different exceptions (ConcurrentModificationException and IllegalStateException).


import java.util.Iterator;
import java.util.HashMap;
import java.util.Map;

public class LoopingFun
{
private static Map<String, String> translatorMap = new HashMap<String,String>();

static
{
translatorMap.put("zero", "cero");
translatorMap.put("one", "uno");
translatorMap.put("two", "dos");
translatorMap.put("three", "tres");
translatorMap.put("four", "cuatro");
translatorMap.put("five", "cinco");
translatorMap.put("six", "seis");
translatorMap.put("seven", "siete");
translatorMap.put("eight", "ocho");
translatorMap.put("nine", "nueve");
translatorMap.put("ten", "diez");
}

public static void main(final String[] arguments)
{
System.out.println("Pre-Size: " + translatorMap.size());
for ( final String translatorKey : translatorMap.keySet() )
{
// Line below is commented out to avoid ConcurrentModificationException
// (cannot remove directly on the Map while iterating; must remove from
// the Iterator, but the enhanced for-each loop "hides" the Iterator).
//translatorMap.remove(translatorKey);
}
final Iterator mapIter = translatorMap.keySet().iterator();
while ( mapIter.hasNext() )
{
// Line below is commented out to avoid IllegalStateException (need to
// call Iterator's hasNext() method first.
//mapIter.remove();

// Line below is commented out to avoid ConcurrentModificationException
// (cannot remove directly on the Map while iterating; must remove from
// the Iterator).
//translatorMap.remove((String)mapIter.next());

mapIter.next();
mapIter.remove();
}
System.out.println("Post-Size: " + translatorMap.size());
}
}


When the code exists as shown above (working with problem lines commented out), the results of running the compiled class look like this:



The next three snapshots show what the runtime output looks like when the appropriate code is commented out and the three commented out examples in the code above are each individually uncommented. Note that these are runtime errors rather than compile time errors.

Because the for-each loop hides the Iterator, one might tempted to call the remove method directly on the Map. When this is done, a ConcurrentModificationException occurs as shown in the next screen snapshot. Note that the line cited in the trace is the line of the for loop rather than the line where the remove() is actually called.



If one tries to call a removal in a more traditional explicit Iterator-driven iteration without first calling Iterator.next(), an IllegalStateException is thrown as shown in the next screen snapshot.



Finally, the last "don't do it this way" example shows trying to call remove directly on the Map rather than on the Iterator. Even though this third example of Collection removal abuse is included in a traditional explicit Iterator-driven iteration loop, it still results in a ConcurrentModificationException because only an Iterator (and not the Collection itself) allows a safe removal during iteration. The final screen snapshot indicates this exception, which in this case shows the line of the remove() call in the trace.



The enhanced for-each loop in Java is a sweet piece of syntactic sugar. It can be used for many of the common, everyday Java Collection iteration/looping needs. However, as this blog entry demonstrated, there are times when it is not appropriate and the more traditional and explicit Iterator-driven iteration must be used instead.

1 comment:

yuhoo said...

Good and useful info. Thanks for sharing :)