原创

map为什么不能遍历的同时进行增删操作

作者:cndz 围观群众:721 更新于 标签:map遍历java

简介

在 Java 中,通常不能在遍历 Map 的过程中直接进行增删操作,这是因为会引发并发修改异常(ConcurrentModificationException)。以前在写代码的时候也知道不能在遍历Map的时候不能直接对map进行put、remove操作。但是一直没有探索原因,就想着今天好好研究一下,加深记忆防止踩坑吧。

错误示例

    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "1");
        map.put(2, "2");
        map.put(3, "3");
        for(Map.Entry<Integer,String> entry : map.entrySet()){
            int key = entry.getKey();
            if(1 == key){
                map.remove(1);
            }
            String value = entry.getValue();
            System.out.println(key + ":" +  value);
        }
    }

引发异常错误信息:

异常引发原因

ConcurrentModificationException 是 Java 集合框架的一种异常,它指示在遍历集合期间,集合的结构被修改了。当你调用 Map 的 iterator() 方法来进行遍历时,会创建一个迭代器来遍历 Map 的元素。

为什么会发生这种异常呢?这是因为迭代器在初始化时会记录 Map 的修改次数,每次进行增删操作时,修改次数都会增加。当修改次数与迭代器记录的次数不一致时,就会抛出异常。

在看源码的时候就会发现,我们使用Iterator进行遍历是都会用到下面这个方法。

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;
        }

上面的modCount是表示map中的元素被修改了几次(在移除,新加元素时此值都会自增),而expectedModCount是表示期望的修改次数,在迭代器构造的时候这两个值是相等,如果在遍历过程中这两个值出现了不同步就会抛出ConcurrentModificationException异常。

深究map的集中remove实现

(1)HashMap本身的remove实现:

public V remove(Object key) {    
    Node<K,V> e;    
    return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}

(2)HashMap.KeySet的remove实现

public final boolean remove(Object key) {    
    return removeNode(hash(key), key, null, false, true) != null;
}

(3)HashMap.EntrySet的remove实现

public final boolean remove(Object o) {    
    if (o instanceof Map.Entry) {        
        Map.Entry<?,?> e = (Map.Entry<?,?>) o;        
        Object key = e.getKey();        
        Object value = e.getValue();        
        return removeNode(hash(key), key, value, true, true) != null;    
        }    
     return false;
 }

(4)HashMap.HashIterator的remove方法实现

public final void remove() {    
    Node<K,V> p = current;    
    if (p == null)        
        throw new IllegalStateException();    
    if (modCount != expectedModCount)        
        throw new ConcurrentModificationException();    
    current = null;   
    K key = p.key;    
    removeNode(hash(key), key, null, false, false);    
    expectedModCount = modCount; //----------------这里将expectedModCount 与modCount进行同步}

以上几种方式都通过调用HashMap.removeNode方法来实现删除key的操作。在removeNode方法内只要移除了key, modCount就会执行一次自增操作,此时modCount就与expectedModCount不一致了;

上面几种remove实现中,只有第三种iterator的remove方法在调用完removeNode方法后同步了expectedModCount值与modCount相同,所以在遍历下个元素调用nextNode方法时,iterator方式不会抛异常。

到这里是不是有一种恍然大明白的感觉呢!

解决办法

为了解决这个问题,有一些可行的方式:

  1. 使用迭代器的 remove() 方法:如果你必须在遍历过程中删除某个元素,可以使用迭代器的 remove() 方法,而不是 Map 的 remove() 方法。迭代器的 remove() 方法可以删除当前迭代器位置指向的元素,并且不会引发异常。
  2. 使用临时集合进行删除:如果需要在遍历完成后进行删除操作,可以使用一个临时集合来保存需要删除的元素,在遍历完成后再统一进行删除操作。
  3. 使用并发安全的 Map:如果需要在遍历过程中进行增删操作,并发安全的 Map 实现类,例如 ConcurrentHashMap,提供了线程安全的遍历和修改操作。不过需要注意的是,并发修改的时间点可能会导致看到添加或删除的元素,但不保证看到更新的值,这取决于并发修改的时机。