现象
当HashMap使用for each遍历entrySet的同时,使用HashMap的remove操作元素时,并不是在并发的情况下,也会抛出异常:ConcurrentModificationException。
复现
示例:
代码语言:javascript复制public static void main(String[] args) {
Map<String, Object> map = new HashMap<>();
map.put("a", 1);
map.put("b", "b");
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getKey().equals("a")){
map.remove("a");
}
}
}
运行结果:
代码语言:javascript复制Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1597)
at java.base/java.util.HashMap$EntryIterator.next(HashMap.java:1630)
at java.base/java.util.HashMap$EntryIterator.next(HashMap.java:1628)
at com.example.springcloudtest.spring.postprocessor.Test.main(Test.java:20)
运行环境:
代码语言:javascript复制java version "17.0.2" 2022-01-18 LTS
Java(TM) SE Runtime Environment (build 17.0.2 8-LTS-86)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.2 8-LTS-86, mixed mode, sharing)
原因
核心原因是:for-each循环遍历entrySet时,使用的是迭代器java.util.HashMap.EntryIterator,而删除元素使用的java.util.HashMap#remove(java.lang.Object)方法是HashMap的,并不是迭代器的方法。使用两种不同的模式操作容器,会使迭代器看到的容器中的元素是不一致的。
我们可以反编译看去除语法糖后的代码:
代码语言:javascript复制public class Test {
public Test() {
}
public static void main(String[] args) {
Map<String, Object> map = new HashMap();
map.put("a", 1);
map.put("b", "b");
Iterator var2 = map.entrySet().iterator();
while(var2.hasNext()) {
Entry<String, Object> entry = (Entry)var2.next();
if (((String)entry.getKey()).equals("a")) {
map.remove("a");
}
}
}
}
具体来说。
当我们使用for-each循环遍历entrySet时:
代码语言:javascript复制 Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
其实使用的数据结构是java.util.HashMap.EntryIterator:
java.util.HashMap.EntrySet#iterator
代码语言:javascript复制public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
循环遍历会隐式调用方法:java.util.HashMap.HashIterator#nextNode,从异常堆栈也可以看出:
代码语言:javascript复制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;
}
代码语言:javascript复制if (modCount != expectedModCount)
expectedModCount是Iterator生成新实例,都会从HashMap的属性modCount获取值。
for-each循环遍历不会更改Iterator实例中expectedModCount值,而HashMap中的modCount值,当使用java.util.HashMap#remove(java.lang.Object)方法时候,会更改。
更改的后果就是if (modCount != expectedModCount)条件不成立,抛出异常。
其实质是迭代器设计模式:单线程环境下,如果使用迭代器遍历容器中的元素,必须使用迭代器删除容器中的元素。
修改后的代码:
代码语言:javascript复制public static void main(String[] args) {
Map<String, Object> map = new HashMap<>();
map.put("a", 1);
map.put("b", "b");
System.out.println(map);
Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String, Object> next = iterator.next();
if (next.getKey().equals("a")){
iterator.remove();
}
}
System.out.println("删除后:" map);
}
正确示例
1、显示使用迭代器
代码语言:javascript复制public static void main(String[] args) {
Map<String, Object> map = new HashMap<>();
map.put("a", 1);
map.put("b", "b");
System.out.println(map);
Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String, Object> next = iterator.next();
if (next.getKey().equals("a")){
iterator.remove();
}
}
System.out.println("删除后:" map);
}
2、使用java.util.Collection#removeIff方法
代码语言:javascript复制public static void main(String[] args) {
Map<String, Object> map = new HashMap<>();
map.put("a", 1);
map.put("b", "b");
map.entrySet().removeIf(entry -> entry.getKey().equals("a"));
System.out.println(map);
}
总结
java中的for-each循环遍历,其实质用的容器的迭代器,当我们遍历容器中的元素时候,不能使用容器本身的remove方法删除元素,这样会导致迭代器看到的数据不一致,而且迭代器也会校验这种情况。