java陷阱之:HashMap for each遍历同时删除,抛出ConcurrentModificationException

2023-06-19 14:32:49 浏览数 (2)


现象


当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方法删除元素,这样会导致迭代器看到的数据不一致,而且迭代器也会校验这种情况。

0 人点赞