当前位置: 首页 > 后端技术 > Java

公司新来的同事:为什么HashMap遍历的时候不能删除?一下子我就懵了!

时间:2023-04-01 18:12:38 Java

作者:你不牛逼\链接:https://juejin.cn/post/7114669787870920734前段时间同事在扫码KW的时候,有这样的留言:上面的原因是即HashMap是用foreach遍历的时候同时进行put赋值操作会出现问题,会出现异常ConcurrentModificationException。于是粗略看了一下,印象是集合类在遍历时删除或添加操作需要谨慎。通常,迭代器用于操作。于是我就跟同事说,应该用迭代器来操作集合元素。同事问我为什么?现在我很困惑?是的,我只记得能不能这样用,但是好像一直没研究过为什么?所以今天决定好好研究一下这个HashMap的遍历操作,防止挖坑!foreach循环?java的foreach语法是jdk1.5新增的特性,主要是作为for语法的增强,那么它的底层是如何实现的呢?我们仔细看看:在foreach语法内部,集合是用iteratoriterator实现的,数组是用下标遍历实现的。Java5及以上编译器隐藏了基于迭代和数组下标遍历的内部实现。(注意,这里说的是“Java编译器”或者说Java语言隐藏了它的实现,并不是说某段Java代码隐藏了它的实现,也就是我们在任意一段JDKJava代码中找到它的实现不隐藏在这里。这里的实现是隐藏在Java编译器中的,看一段foreachJava代码编译出来的字节码,猜猜是怎么实现的)下面写个例子研究一下:publicclassHashMapIteratorDemo{String[]arr={"aa","bb","抄送"};publicvoidtest1(){for(Stringstr:arr){}}}将上面的例子转化为字节码并反编译(主函数部分):可能我们并不清楚这些指令到底做了什么,但是我们可以对比一下字节码指令由以下代码生成:publicclassHashMapIteratorDemo2{String[]arr={"aa","bb","cc"};publicvoidtest1(){for(inti=0;ilist=newArrayList<>();publicvoidtest1(){list.add(1);列表.添加(2);列表.添加(3);for(Integervar:list){}}}通过Iterator遍历集合:publicclassHashMapIteratorDemo4{Listlist=newArrayList<>();公共无效测试1(){列表.添加(1);列表.添加(2);列表.添加(3);迭代器<整数>it=list.iterator();while(it.hasNext()){整数var=it.next();}}}比较两种方法的字节码如下:我们发现两种方法的字节码指令操作几乎完全一样;因此我们可以得出以下结论:对于集合,由于集合实现了Iterator迭代器,foreach语法最终被编译器转换为对Iterator.next()的调用;对于数组,它被转换为对数组中每个元素的循环引用HashMap遍历集合,对集合元素进行remove、put、add1、现象。根据上面的分析,我们知道HashMap底层实现了Iterator迭代器,所以理论上我们也可以使用迭代器来遍历,确实如此,例如下面:publicclassHashMapIteratorDemo5{publicstaticvoidmain(String[]args){Mapmap=newHashMap<>();map.put(1,"aa");map.put(2,"bb");map.put(3,"抄送");for(Map.Entryentry:map.entrySet()){intk=entry.getKey();字符串v=entry.getValue();System.out.println(k+"="+v);}}}输出:ok,遍历没有问题,那么操作集合元素remove,put,add呢?publicclassHashMapIteratorDemo5{publicstaticvoidmain(String[]args){Mapmap=newHashMap<>();map.put(1,"aa");map.put(2,"bb");map.put(3,"抄送");for(Map.Entryentry:map.entrySet()){intk=entry.getKey();如果(k==1){map.put(1,“AA”);}字符串v=entry.getValue();System.out.println(k+"="+v);}}}执行结果:Execution没问题,put操作也成功了但是!但!但!问题来了!!!我们知道HashMap是一个线程不安全的集合类。如果使用foreach来遍历,增删操作会引发java.util.ConcurrentModificationException。put操作可能会抛出此异常。(为什么可以,后面会解释)为什么会抛出这个异常?我们先看看javaapi文档中对HasMap操作的解释。翻译大概意思就是这个方法返回这个映射中包含的键的集合视图。集合由映射支持,如果在迭代集合时修改了映射(除了通过迭代器自己的删除操作),迭代的结果是不确定的。集合支持元素移除,它通过Iterator.remove、set.remove、removeAll、retainal和clear操作从映射中移除相应的映射。简单来说,通过map.entrySet()遍历集合时,不能对集合本身进行remove、add等操作,需要使用迭代器进行操作。对于put操作,如果像上例那样替换操作修改了第一个元素,则不会抛出异常,但如果是使用put添加元素的操作,则肯定会抛出异常。让我们修改上面的例子:map.put(1,"aa");地图.put(2,"bb");map.put(3,"抄送");for(Map.Entryentry:map.entrySet()){intk=entry.getKey();如果(k==1){map.put(4,"AA");}字符串v=entry.getValue();System.out.println(k+"="+v);}}}执行异常:this验证了上面提到的put操作可能会抛出java.util.ConcurrentModificationException异常。但我有疑问。我们上面说了foreach循环是通过迭代器遍历?为什么不能来这里?这个其实很简单,原因是我们的遍历操作底层确实是通过迭代器进行的,但是我们的remove等操作是直接操作map进行的,如上例:map.put(4,"AA");//这里实际操作的是直接在集合上,而不是通过迭代器。所以还是会出现ConcurrentModificationException异常。2、先了解一下底层原理,再看看HashMap的源码。通过源码我们发现在使用Iterator遍历集合时会用到这个方法:finalNodenextNode(){Node[]t;节点e=next;如果(modCount!=expectedModCount)抛出新的ConcurrentModificationException();如果(e==null)抛出新的NoSuchElementException();if((next=(current=e).next)==null&&(t=table)!=null){do{}while(indexe;return(e=removeNode(hash(key),key,null,false,true))==null?null:e.value;}(2)HashMap.KeySet的remove实现publicfinalbooleanremove(Objectkey){returnremoveNode(hash(key),key,null,false,true)!=null;}(3)HashMap.EntrySet的删除现实publicfinalbooleanremove(Objecto){if(oinstanceofMap.Entry){Map.Entrye=(Map.Entry)o;对象键=e.getKey();对象值=e.getValue();returnremoveNode(hash(key),key,value,true,true)!=null;}returnfalse;}(4)HashMap.HashIterator的remove方法实现publicfinalvoidremove(){Nodep=current;如果(p==null)抛出新的IllegalStateException();如果(modCount!=expectedModCount)抛出新的ConcurrentModificationException();当前=空;Kkey=p.key;removeNode(散列(键),键,空,假,错误的);expectedModCount=modCount;//----------------这里expectedModCount和modCount是同步的}以上四个方法都是通过调用HashMap.removeNode方法实现删除key操作只要在removeNode方法中移除了key,modCount就会进行一次自增操作。此时modCount与expectedModCount不一致;finalNoderemoveNode(inthash,Objectkey,Objectvalue,booleanmatchValue,booleanmovable){Node[]tab;节点p;intn,指数;if((tab=table)!=null&&(n=tab.length)>0&&...if(node!=null&&(!matchValue||(v=node.value)==value||(value!=null&&value.equals(v)))){if(nodeinstanceofTreeNode)((TreeNode)node).removeTreeNode(this,tab,movable);elseif(node==p)tab[index]=node.next;elsep.next=node.next;++modCount;//----------------------这里modCount是自增的,可能会导致后面和expectedModCount不一致--size;afterNodeRemoval(node);returnnode;}}returnnull;}以上三个remove实现中,只有第三个iterator的remove方法在调用removeNode方法后同步了expectedModCount和modCount的值,所以在遍历下一个元素调用nextNode方法时,iterator方法不会抛出异常这里有没有恍然大悟的感觉?因此,如果在遍历集合时需要对元素进行操作,就需要使用Iterator迭代器,如下:<>();map.put(1,"aa");map.put(2,"bb");map.put(3,"抄送");//for(Map.Entryentry:map.entrySet()){//intk=entry.getKey();////if(k==1){//map.put(1,"AA");//}//Stringv=entry.获取值();//System.out.println(k+"="+v);//}Iterator>it=map.entrySet().iterator();while(it.hasNext()){Map.Entryentry=it.next();intkey=entry.getKey();if(key==1){it.remove();}}}}近期热点文章推荐:1.1000+Java面试题及答案(2022最新版)2.太棒了!Java协程来了。.3.SpringBoot2.x教程,太全面了!4.不要用爆破爆满画面,试试装饰者模式,这才是优雅的方式!!5.《Java开发手册(嵩山版)》最新发布,赶快下载吧!感觉不错,别忘了点赞+转发!