公司新来一个同事:为什么 HashMap 不能一边遍历一边删除?一下子把我问懵了!(2)

简介: 公司新来一个同事:为什么 HashMap 不能一边遍历一边删除?一下子把我问懵了!

HashMap遍历集合并对集合元素进行remove、put、add

1、现象


根据以上分析,我们知道HashMap底层是实现了Iterator迭代器的 ,那么理论上我们也是可以使用迭代器进行遍历的,这倒是不假,例如下面:

public class HashMapIteratorDemo5 {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "aa");
        map.put(2, "bb");
        map.put(3, "cc");
        for(Map.Entry<Integer, String> entry : map.entrySet()){
            int k=entry.getKey();
            String v=entry.getValue();
            System.out.println(k+" = "+v);
        }
    }
}


输出:


image.png


ok,遍历没有问题,那么操作集合元素remove、put、add呢?


public class HashMapIteratorDemo5 {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "aa");
        map.put(2, "bb");
        map.put(3, "cc");
        for(Map.Entry<Integer, String> entry : map.entrySet()){
            int k=entry.getKey();
            if(k == 1) {
                map.put(1, "AA");
            }
            String v=entry.getValue();
            System.out.println(k+" = "+v);
        }
    }
}



执行结果:


image.png


执行没有问题,put操作也成功了。


但是!但是!但是!问题来了!!!


我们知道HashMap是一个线程不安全的集合类,如果使用foreach遍历时,进行add,remove操作会java.util.ConcurrentModificationException异常。put操作可能会抛出该异常。(为什么说可能,这个我们后面解释)


为什么会抛出这个异常呢?


我们先去看一下java api文档对HasMap操作的解释吧。


image.png


翻译过来大致的意思就是该方法是返回此映射中包含的键的集合视图。集合由映射支持,如果在对集合进行迭代时修改了映射(通过迭代器自己的移除操作除外),则迭代的结果是未定义的。集合支持元素移除,通过Iterator.remove、set.remove、removeAll、retainal和clear操作从映射中移除相应的映射。简单说,就是通过map.entrySet()这种方式遍历集合时,不能对集合本身进行remove、add等操作,需要使用迭代器进行操作。


对于put操作,如果这个操作时替换操作如上例中将第一个元素进行修改,就没有抛出异常,但是如果是使用put添加元素的操作,则肯定会抛出异常了。我们把上面的例子修改一下:


public class HashMapIteratorDemo5 {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "aa");
        map.put(2, "bb");
        map.put(3, "cc");
        for(Map.Entry<Integer, String> entry : map.entrySet()){
            int k=entry.getKey();
            if(k == 1) {
                map.put(4, "AA");
            }
            String v=entry.getValue();
            System.out.println(k+" = "+v);
        }
    }
}




执行出现异常:


image.png



这就是验证了上面说的put操作可能会抛出java.util.ConcurrentModificationException异常。


但是有疑问了,我们上面说过foreach循环就是通过迭代器进行的遍历啊?为什么到这里是不可以了呢?


这里其实很简单,原因是我们的遍历操作底层确实是通过迭代器进行的,但是我们的remove等操作是通过直接操作map进行的,如上例子:map.put(4, "AA");//这里实际还是直接对集合进行的操作,而不是通过迭代器进行操作。所以依然会存在ConcurrentModificationException异常问题。


2、细究底层原理

我们再去看看HashMap的源码,通过源代码,我们发现集合在使用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异常。


现在我们来看看集合remove操作:


(1)HashMap本身的remove实现:


image.png


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不一致了;


final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        ...
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;   //------------------------这里对modCount进行了自增,可能会导致后面与expectedModCount不一致
            --size;
            afterNodeRemoval(node);
            return node;
        }
        }
        return null;
   }




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


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


所以,如果需要对集合遍历时进行元素操作需要借助Iterator迭代器进行,如下:


public class HashMapIteratorDemo5 {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "aa");
        map.put(2, "bb");
        map.put(3, "cc");
        //        for(Map.Entry<Integer, String> entry : map.entrySet()){  //            int k=entry.getKey();  //            //            if(k == 1) {//                map.put(1, "AA");//            }//            String v=entry.getValue();  //            System.out.println(k+" = "+v);  //        }
        Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator();
        while(it.hasNext()){
            Map.Entry<Integer, String> entry = it.next();
            int key=entry.getKey();
            if(key == 1){
                it.remove();
            }
        }
    }
}

image.png

image.png

相关文章
|
安全 Java API
java中HashMap的七种遍历方式
java.util.ConcurrentModificationException , 这种办法是非安全的 , 我们可以使用Iterator.remove() ,或者是Lambda 中的 removeIf() , 或者是Stream 中的 filter() 过滤或者删除相关数据
126 1
|
7月前
|
Java API
面试官上来就让手撕HashMap的7种遍历方式,当场愣住,最后只写出了3种
面试官上来就让手撕HashMap的7种遍历方式,当场愣住,最后只写出了3种
47 1
|
存储 算法 安全
HashMap的遍历方式及底层原理
HashMap的遍历方式及底层原理
遍历HashMap的四种方式
遍历HashMap的四种方式
87 0
|
JavaScript 小程序 Java
HashMap 为什么不能一边遍历一遍删除
HashMap 为什么不能一边遍历一遍删除
|
2月前
|
Java
让星星⭐月亮告诉你,HashMap中保证红黑树根节点一定是对应链表头节点moveRootToFront()方法源码解读
当红黑树的根节点不是其对应链表的头节点时,通过调整指针的方式将其移动至链表头部。具体步骤包括:从链表中移除根节点,更新根节点及其前后节点的指针,确保根节点成为新的头节点,并保持链表结构的完整性。此过程在Java的`HashMap$TreeNode.moveRootToFront()`方法中实现,确保了高效的数据访问与管理。
31 2
|
2月前
|
Java 索引
让星星⭐月亮告诉你,HashMap之往红黑树添加元素-putTreeVal方法源码解读
本文详细解析了Java `HashMap` 中 `putTreeVal` 方法的源码,该方法用于在红黑树中添加元素。当数组索引位置已存在红黑树类型的元素时,会调用此方法。具体步骤包括:从根节点开始遍历红黑树,找到合适位置插入新元素,调整节点指针,保持红黑树平衡,并确保根节点是链表头节点。通过源码解析,帮助读者深入理解 `HashMap` 的内部实现机制。
41 2
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
65 0
|
2月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
64 5