众所周知,HashMap在迭代时remove会抛出异常,ConcurrentModificationException,但事实真的是这样的吗?的确会抛异常,但也有特例。废话少说,上代码:
public class ConcurrentModificationException { public static void main(String[] args) { HashMap<Integer, Integer> map = new HashMap<>(); map.put(1, 2); for (Map.Entry<Integer, Integer> entry : map.entrySet()) { map.remove(1); } System.out.println(map); } }运行以上代码,我们将会发现,程序并没有报ConcurrentModificationException异常,而且输出:{}。
这与常识不服啊?想要弄懂其中缘由,得理解程序何时会抛ConcurrentModificationException异常。通过HashMap的源码可知,当modCount != expectedModCount时,此异常将会被抛出。
- if (modCount != expectedModCount)
- throw new ConcurrentModificationException();
public final boolean hasNext() {
return next != null;
}
但会false,并没有检测modCount != expectedModCount,也就不会抛异常了。
但是,如果我们在上面的代码中再加一行添加键值对的语句:map.put(3, 4);也就是说map中有两个键值对,这时你会发现程序将抛出ConcurrentModificationException异常,这又是为什么呢?还是来看源码就知道了。
执行remove之后,
接着遍历map.entrySet(),执行到
public final boolean hasNext() { return next != null; }
返回true,接着执行
final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> { public final Map.Entry<K,V> next() { return nextNode(); } }
跳转到nextNode()中,执行
final Node<K,V> nextNode() { Node<K,V>[] t; Node<K,V> e = next; if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (e == null) throw new NoSuchElementException(); if ((next = (current = e).next) == null && (t = table) != null) { do {} while (index < t.length && (next = t[index++]) == null); } return e; }
看到了吧,上面的
if (modCount != expectedModCount) throw new ConcurrentModificationException();异常就在这里抛出了。
如果进一步深究,你会发现一个更有趣的事,如果你迭代的代码是这样的,Java8的Lambda表达式
map.forEach((key, value) -> { System.out.println(key + "==" + value); map.remove(1); // ConcurrentModificationException });你会发现,即使你的map中只有一个键值对,程序也将抛出异常,这与foeEach和lambda迭代的内部原理有关了,同样,调试源码将找到答案。
在执行remove后,程序将进入
@Override public void forEach(BiConsumer<? super K, ? super V> action) { Node<K,V>[] tab; if (action == null) throw new NullPointerException(); if (size > 0 && (tab = table) != null) { int mc = modCount; for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) action.accept(e.key, e.value); } if (modCount != mc) throw new ConcurrentModificationException(); } }
在这里if (modCount != mc)将抛出异常。
总结:
HashMap迭代时Remove掉map中包含的键值对,从而改变结构(modCount加1),接下来程序若再有检测modCount的fast-fail机制,程序便将抛出ConcurrentModificationException异常。(注:如果上面程序,remove(6) 将不会引发HashMap结构性改变,也就不会抛异常了)
如果你还了解HashMap的更多源码分析,请移步HashMap源码分析(jdk1.8)。