迭代HashMap时remove()与put()方法的行为差异探究
嘿,这个问题问到点子上了!这背后其实是HashMap迭代器的**fail-fast(快速失败)**机制在搞鬼,而且remove和put的行为差异,本质是它们对HashMap内部modCount的影响以及迭代器的检查时机不同。
先搞懂fail-fast的核心逻辑
HashMap内部维护了一个modCount变量,用来记录哈希表结构被修改的次数(比如新增、删除元素、扩容这些操作都会改变它)。当你通过keySet()、entrySet()获取迭代器时,迭代器会初始化一个expectedModCount变量,把当时的modCount值存进去。
之后每次调用迭代器的next()方法时,都会先检查modCount和expectedModCount是否相等:
- 如果相等,正常返回下一个元素;
- 如果不等,直接抛出
ConcurrentModificationException(也就是我们常说的“并发修改异常”)——这就是fail-fast机制,用来防止迭代过程中哈希表结构被意外修改导致迭代器错乱。
为什么remove()有时候看起来“没事”,有时候又抛异常?
你代码里用的是myMap.remove("2")这种直接调用HashMap的remove方法,而不是迭代器的iterator.remove()。这种情况下:
- 调用
myMap.remove()会让modCount加1,但迭代器的expectedModCount不会同步更新; - 如果删除操作之后,迭代器已经没有下一个元素要遍历了(比如你删的是最后一个元素,或者循环刚好走到末尾),那么迭代器的
hasNext()会返回false,循环直接结束,不会触发next()里的检查,所以看起来没抛异常; - 但如果删除后还有元素要遍历,下一次调用
next()时就会发现modCount != expectedModCount,立刻抛出异常。
⚠️ 注意:就算这次没抛异常,这种操作也是不安全的——迭代器的内部指针可能已经错乱,后续遍历可能出现遗漏或重复元素。正确的删除方式是用迭代器自己的remove()方法,它会在删除后同步更新expectedModCount = modCount,避免触发异常。
put()的行为为什么不一样?
put()方法对modCount的影响分两种情况:
- 替换已有元素:如果put的key已经存在,只是修改value,这种操作不会改变哈希表的结构,所以
modCount不会变化。这时候迭代器的检查不会触发异常,看起来“没事”; - 新增元素:如果put的key是新的,会触发哈希表结构变化(甚至可能触发扩容),
modCount会加1。这时候只要迭代器后续调用next(),就会触发ConcurrentModificationException——而且因为新增元素可能改变哈希表的桶结构,迭代器的错乱会比删除更严重。
另外,就算你用迭代器的remove()方法,直接调用myMap.put()还是会触发异常,因为迭代器的方法只能处理自己的删除操作,管不了put带来的结构变化。
结合你的代码看具体场景
你在第一个循环里,当key是"3"时:
myMap.put("3new", "new3")是新增元素,modCount加1;myMap.remove("2")是删除元素,modCount再加1;
这时候modCount比迭代器初始化时的expectedModCount大2。当循环继续执行下一次next()时,就会触发并发修改异常。
正确的迭代修改姿势
如果要在迭代时安全修改HashMap,应该用迭代器的API:
Iterator<String> keyIterator = myMap.keySet().iterator(); while (keyIterator.hasNext()) { String key = keyIterator.next(); if (key.equals("3")) { // 如果你必须新增元素,建议先收集要新增的内容,迭代结束后再批量put // myMap.put(key + "new", "new3"); // 这个操作依然会触发异常,除非是替换已有元素 keyIterator.remove(); // 这个是安全的,会同步expectedModCount } }
如果需要在迭代时新增元素,最好的方式是先把要新增的键值对存到一个临时集合里,等迭代完全结束后,再批量put到原HashMap中。
内容的提问来源于stack exchange,提问作者user2681668

