作者:你呀不牛 \
链接: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开发手册(嵩山版)》最新公布,速速下载!

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