乐趣区

关于java:公司新来一个同事为什么-HashMap-不能一边遍历一边删除一下子把我问懵了

作者:你呀不牛 \
链接:https://juejin.cn/post/7114669787870920734

前段时间,共事在代码中 KW 扫描的时候呈现这样一条:

下面呈现这样的起因是在应用 foreach 对 HashMap 进行遍历时,同时进行 put 赋值操作会有问题,异样 ConcurrentModificationException。

于是帮同简略的看了一下,印象中汇合类在进行遍历时同时进行删除或者增加操作时须要审慎,个别应用迭代器进行操作。

于是通知共事,应该应用迭代器 Iterator 来对汇合元素进行操作。共事问我为什么?这一下子把我问蒙了?对啊,只是记得这样用不能够,然而如同本人素来没有细究过为什么?

于是明天决定把这个 HashMap 遍历操作好好地钻研一番,避免采坑!

foreach 循环?

java foreach 语法是在 jdk1.5 时退出的新个性,次要是当作 for 语法的一个加强,那么它的底层到底是怎么实现的呢?上面咱们来好好钻研一下:

foreach 语法外部,对 collection 是用 iterator 迭代器来实现的,对数组是用下标遍从来实现。Java 5 及以上的编译器暗藏了基于 iteration 和数组下标遍历的外部实现。

(留神,这里说的是“Java 编译器”或 Java 语言对其实现做了暗藏,而不是某段 Java 代码对其实现做了暗藏,也就是说,咱们在任何一段 JDK 的 Java 代码中都找不到这里被暗藏的实现。这里的实现,暗藏在了 Java 编译器中,查看一段 foreach 的 Java 代码编译成的字节码,从中推测它到底是怎么实现的了)

咱们写一个例子来钻研一下:

public class HashMapIteratorDemo {String[] arr = {"aa", "bb", "cc"};

    public void test1() {for(String str : arr) {}}
}

将下面的例子转为字节码反编译一下(主函数局部):

兴许咱们不能很分明这些指令到底有什么作用,然而咱们能够比照一下上面段代码产生的字节码指令:

public class HashMapIteratorDemo2 {String[] arr = {"aa", "bb", "cc"};

    public void test1() {for(int i = 0; i < arr.length; i++) {String str = arr[i];
        }
    }
}

看看两个字节码文件,有木有发现指令简直雷同,如果还有疑难咱们再看看对汇合的 foreach 操作:

通过 foreach 遍历汇合:

public class HashMapIteratorDemo3 {List<Integer> list = new ArrayList<>();

    public void test1() {list.add(1);
        list.add(2);
        list.add(3);

        for(Integer var : list) {}}
}

通过 Iterator 遍历汇合:

public class HashMapIteratorDemo4 {List<Integer> list = new ArrayList<>();

    public void test1() {list.add(1);
        list.add(2);
        list.add(3);

        Iterator<Integer> it = list.iterator();
        while(it.hasNext()) {Integer var = it.next();
        }
    }
}

将两个办法的字节码比照如下:

咱们发现两个办法字节码指令操作简直截然不同;

这样咱们能够得出以下论断:

对汇合来说,因为汇合都实现了 Iterator 迭代器,foreach 语法最终被编译器转为了对 Iterator.next() 的调用;

对于数组来说,就是转化为对数组中的每一个元素的循环援用。

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

输入:

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

执行后果:

执行没有问题,put 操作也胜利了。

然而!然而!然而!问题来了!!!

咱们晓得 HashMap 是一个线程不平安的汇合类,如果应用 foreach 遍历时,进行 add,remove 操作会 java.util.ConcurrentModificationException 异样。put 操作可能会抛出该异样。(为什么说可能,这个咱们前面解释)

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

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

翻译过去大抵的意思就是该办法是返回此映射中蕴含的键的汇合视图。汇合由映射反对,如果在对汇合进行迭代时批改了映射(通过迭代器本人的移除操作除外),则迭代的后果是未定义的。汇合反对元素移除,通过 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);
        }

    }
}

执行出现异常:

这就是验证了下面说的 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 实现:

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

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿 (2022 最新版)

2. 劲爆!Java 协程要来了。。。

3.Spring Boot 2.x 教程,太全了!

4. 别再写满屏的爆爆爆炸类了,试试装璜器模式,这才是优雅的形式!!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

退出移动版