为什么在迭代期间从地图中删除不存在的元素有时只会崩溃?

Why does removing a non-existent element from a map during iteration only crash sometimes?

注意:这不是询问如何在迭代期间从地图中删除项目的许多问题的重复。

我在使用哈希映射迭代器从映射中删除项目时遇到了一些令人惊讶的边缘情况。

以下代码因 ConcurrentModificationException.

而崩溃
Map<Integer, Integer> m = new HashMap<>();
m.put(1, 1);
m.put(2, 2);
m.put(3, 3);

for (Iterator<Map.Entry<Integer, Integer>> iterator = m.entrySet().iterator(); iterator.hasNext(); ) {
    Map.Entry<Integer, Integer> e = iterator.next();
    if (e.getKey() == 2) {
        iterator.remove();
    }
    m.remove(2); // This causes the crash
}

毫不奇怪,以下代码不会:

Map<Integer, Integer> m = new HashMap<>();
m.put(1, 1);
m.put(2, 2);
m.put(3, 3);

for (Iterator<Map.Entry<Integer, Integer>> iterator = m.entrySet().iterator(); iterator.hasNext(); ) {
    Map.Entry<Integer, Integer> e = iterator.next();
    if (e.getKey() == 2) {
        iterator.remove();
    }
    m.remove(4); // No crash here
}

但是,下面的代码也不会崩溃:

Map<Integer, Integer> m = new HashMap<>();
m.put(2, 2);
m.put(3, 3);

for (Iterator<Map.Entry<Integer, Integer>> iterator = m.entrySet().iterator(); iterator.hasNext(); ) {
    Map.Entry<Integer, Integer> e = iterator.next();
    if (e.getKey() == 2) {
        iterator.remove();
    }
    m.remove(2); // Also no crash?
}

第一个和第三个示例之间的唯一区别是删除了 <1, 1> 条目。为什么调用 Map.remove 有时只会崩溃?这是 在标准的任何地方指定?

第一个示例抛出 ConcurrentModificationException,因为您在地图迭代期间调用了 remove 方法。事情的来龙去脉是这样的。

  1. 调用 next() 检索条目 (1, 1)。关键不是2,所以不要调用iterator.remove()。通过调用 m.remove(2) 直接从映射中删除键为 2 的条目。这会更改 Iterator 期望保持不变的内部修改计数。
  2. 调用 next() 检索下一个条目。映射中的修改计数不再与创建时 Iterator 记录的预期修改计数相匹配,因此抛出 ConcurrentModificationException

最后一个例子没有抛出,因为remove(2);已经没有效果了。

  1. 调用 next() 检索条目 (2, 2)。密钥是 2,因此调用 iterator.remove(),删除条目。这具有修改迭代器的预期修改计数以匹配映射的修改计数的效果。调用 m.remove(2) 没有任何效果,因为键 2 不再存在于映射中。
  2. 调用 next() 检索条目 (3, 3)。映射中的修改计数与 Iterator 记录的预期修改计数相匹配,因此不会抛出 ConcurrentModificationException。关键不是2,所以不会调用iterator.remove()。调用 m.remove(2) 无效,因为映射中不存在键 2

请注意,上面的事件序列对于 Java HashMap 迭代其条目的当前方式有效。密钥恰好按顺序检索。通常,对于任何范围的有效整数键,键都保证按顺序检索。