前段時(shí)間,同事在代碼中 KW 掃描的時(shí)候出現(xiàn)這樣一條:
上面出現(xiàn)這樣的原因是在使用 foreach 對(duì) HashMap 進(jìn)行遍歷時(shí),同時(shí)進(jìn)行 put 賦值操作會(huì)有問題,異常 ConcurrentModificationException。
于是幫同簡(jiǎn)單的看了一下,印象中集合類在進(jìn)行遍歷時(shí)同時(shí)進(jìn)行刪除或者添加操作時(shí)需要謹(jǐn)慎,一般使用迭代器進(jìn)行操作。
于是告訴同事,應(yīng)該使用迭代器 Iterator 來對(duì)集合元素進(jìn)行操作。同事問我為什么?這一下子把我問蒙了?對(duì)啊,只是記得這樣用不可以,但是好像自己從來沒有細(xì)究過為什么?
于是今天決定把這個(gè) HashMap 遍歷操作好好地研究一番,防止采坑!
foreach 循環(huán)?
Java foreach 語(yǔ)法是在 JDK 1.5 時(shí)加入的新特性,主要是當(dāng)作 for 語(yǔ)法的一個(gè)增強(qiáng),那么它的底層到底是怎么實(shí)現(xiàn)的呢?下面我們來好好研究一下:
foreach 語(yǔ)法內(nèi)部,對(duì) collection 是用 iterator 迭代器來實(shí)現(xiàn)的,對(duì)數(shù)組是用下標(biāo)遍歷來實(shí)現(xiàn)。Java 5 及以上的編譯器隱藏了基于 iteration 和數(shù)組下標(biāo)遍歷的內(nèi)部實(shí)現(xiàn)。
注意:這里說的是“Java 編譯器”或 Java 語(yǔ)言對(duì)其實(shí)現(xiàn)做了隱藏,而不是某段 Java 代碼對(duì)其實(shí)現(xiàn)做了隱藏,也就是說,我們?cè)谌魏我欢?JDK 的 Java 代碼中都找不到這里被隱藏的實(shí)現(xiàn)。這里的實(shí)現(xiàn),隱藏在了Java 編譯器中,查看一段 foreach 的 Java 代碼編譯成的字節(jié)碼,從中揣測(cè)它到底是怎么實(shí)現(xiàn)的了。
我們寫一個(gè)例子來研究一下:
publicclassHashMapIteratorDemo{ String[]arr={ "aa", "bb", "cc" }; publicvoidtest1(){ for(Stringstr:arr){} } }
將上面的例子轉(zhuǎn)為字節(jié)碼反編譯一下(主函數(shù)部分):
也許我們不能很清楚這些指令到底有什么作用,但是我們可以對(duì)比一下下面段代碼產(chǎn)生的字節(jié)碼指令:
publicclassHashMapIteratorDemo2{ String[]arr={ "aa", "bb", "cc" }; publicvoidtest1(){ for(inti=0;i
看看兩個(gè)字節(jié)碼文件,有木有發(fā)現(xiàn)指令幾乎相同,如果還有疑問我們?cè)倏纯磳?duì)集合的 foreach 操作:
通過 foreach 遍歷集合:
publicclassHashMapIteratorDemo3{ Listlist=newArrayList>(); publicvoidtest1(){ list.add(1); list.add(2); list.add(3); for(Integer var:list){} } }
通過 Iterator 遍歷集合:
publicclassHashMapIteratorDemo4{ Listlist=newArrayList>(); publicvoidtest1(){ list.add(1); list.add(2); list.add(3); Iteratorit=list.iterator(); while(it.hasNext()){ Integer var=it.next(); } } }
將兩個(gè)方法的字節(jié)碼對(duì)比如下:
我們發(fā)現(xiàn)兩個(gè)方法字節(jié)碼指令操作幾乎一模一樣;
這樣我們可以得出以下結(jié)論:
對(duì)集合來說,由于集合都實(shí)現(xiàn)了 Iterator 迭代器,foreach 語(yǔ)法最終被編譯器轉(zhuǎn)為了對(duì) Iterator.next() 的調(diào)用;
對(duì)于數(shù)組來說,就是轉(zhuǎn)化為對(duì)數(shù)組中的每一個(gè)元素的循環(huán)引用。
HashMap 遍歷集合并對(duì)集合元素進(jìn)行 remove、put、add
1、現(xiàn)象
根據(jù)以上分析,我們知道 HashMap 底層是實(shí)現(xiàn)了 Iterator 迭代器的 ,那么理論上我們也是可以使用迭代器進(jìn)行遍歷的,這倒是不假,例如下面:
publicclassHashMapIteratorDemo5{ publicstaticvoidmain(String[]args){ Mapmap=newHashMap>(); map.put(1,"aa"); map.put(2,"bb"); map.put(3,"cc"); for(Map.Entryentry:map.entrySet()){ intk=entry.getKey(); Stringv=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,"cc"); for(Map.Entryentry:map.entrySet()){ intk=entry.getKey(); if(k==1){ map.put(1,"AA"); } Stringv=entry.getValue(); System.out.println(k+"="+v); } } }
執(zhí)行結(jié)果:
執(zhí)行沒有問題,put 操作也成功了。
但是!但是!但是!問題來了!?。?/p>
我們知道 HashMap 是一個(gè)線程不安全的集合類,如果使用 foreach 遍歷時(shí),進(jìn)行add, remove 操作會(huì) java.util.ConcurrentModificationException 異常。put 操作可能會(huì)拋出該異常。(為什么說可能,這個(gè)我們后面解釋)
為什么會(huì)拋出這個(gè)異常呢?
我們先去看一下 Java API 文檔對(duì) HasMap 操作的解釋吧。
翻譯過來大致的意思就是:該方法是返回此映射中包含的鍵的集合視圖。
集合由映射支持,如果在對(duì)集合進(jìn)行迭代時(shí)修改了映射(通過迭代器自己的移除操作除外),則迭代的結(jié)果是未定義的。集合支持元素移除,通過 Iterator.remove、set.remove、removeAll、retainal 和 clear 操作從映射中移除相應(yīng)的映射。簡(jiǎn)單說,就是通過 map.entrySet() 這種方式遍歷集合時(shí),不能對(duì)集合本身進(jìn)行 remove、add 等操作,需要使用迭代器進(jìn)行操作。
對(duì)于 put 操作,如果這個(gè)操作時(shí)替換操作如上例中將第一個(gè)元素進(jìn)行修改,就沒有拋出異常,但是如果是使用 put 添加元素的操作,則肯定會(huì)拋出異常了。我們把上面的例子修改一下:
publicclassHashMapIteratorDemo5{ publicstaticvoidmain(String[]args){ Mapmap=newHashMap>(); map.put(1,"aa"); map.put(2,"bb"); map.put(3,"cc"); for(Map.Entryentry:map.entrySet()){ intk=entry.getKey(); if(k==1){ map.put(4,"AA"); } Stringv=entry.getValue(); System.out.println(k+"="+v); } } }
執(zhí)行出現(xiàn)異常:
這就是驗(yàn)證了上面說的 put 操作可能會(huì)拋出 java.util.ConcurrentModificationException 異常。
但是有疑問了,我們上面說過 foreach 循環(huán)就是通過迭代器進(jìn)行的遍歷???為什么到這里是不可以了呢?
這里其實(shí)很簡(jiǎn)單,原因是我們的遍歷操作底層確實(shí)是通過迭代器進(jìn)行的,但是我們的 remove 等操作是通過直接操作 map 進(jìn)行的,如上例子:map.put(4, "AA"); //這里實(shí)際還是直接對(duì)集合進(jìn)行的操作,而不是通過迭代器進(jìn)行操作。所以依然會(huì)存在 ConcurrentModificationException 異常問題。
2、細(xì)究底層原理
我們?cè)偃タ纯?HashMap 的源碼,通過源代碼,我們發(fā)現(xiàn)集合在使用 Iterator 進(jìn)行遍歷時(shí)都會(huì)用到這個(gè)方法:
finalNodenextNode(){ Node[]t; Nodee=next; if(modCount!=expectedModCount) thrownewConcurrentModificationException(); if(e==null) thrownewNoSuchElementException(); if((next=(current=e).next)==null&&(t=table)!=null){ do{}while(index
這里 modCount 是表示 map 中的元素被修改了幾次(在移除,新加元素時(shí)此值都會(huì)自增),而 expectedModCount 是表示期望的修改次數(shù),在迭代器構(gòu)造的時(shí)候這兩個(gè)值是相等,如果在遍歷過程中這兩個(gè)值出現(xiàn)了不同步就會(huì)拋出 ConcurrentModificationException 異常。
現(xiàn)在我們來看看集合 remove 操作:
(1)HashMap 本身的 remove 實(shí)現(xiàn):
publicVremove(Objectkey){ Nodee; return(e=removeNode(hash(key),key,null,false,true))==null? null:e.value; }
(2)HashMap.KeySet 的 remove 實(shí)現(xiàn)
publicfinalbooleanremove(Objectkey){ returnremoveNode(hash(key),key,null,false,true)!=null; }
(3)HashMap.EntrySet 的 remove 實(shí)現(xiàn)
publicfinalbooleanremove(Objecto){ if(oinstanceofMap.Entry){ Map.Entry<??,???>e=(Map.Entry<??,???>)o; Objectkey=e.getKey(); Objectvalue=e.getValue(); returnremoveNode(hash(key),key,value,true,true)!=null; } returnfalse; }
(4)HashMap.HashIterator 的 remove 方法實(shí)現(xiàn)
publicfinalvoidremove(){ Nodep=current; if(p==null) thrownewIllegalStateException(); if(modCount!=expectedModCount) thrownewConcurrentModificationException(); current=null; Kkey=p.key; removeNode(hash(key),key,null,false,false); expectedModCount=modCount;//--這里將expectedModCount與modCount進(jìn)行同步 }
以上四種方式都通過調(diào)用 HashMap.removeNode 方法來實(shí)現(xiàn)刪除key的操作。在 removeNode 方法內(nèi)只要移除了 key, modCount 就會(huì)執(zhí)行一次自增操作,此時(shí) modCount 就與 expectedModCount 不一致了;
finalNoderemoveNode(inthash,Objectkey,Objectvalue, booleanmatchValue,booleanmovable){ Node[]tab; Nodep; intn,index; 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; else p.next=node.next; ++modCount;//----這里對(duì)modCount進(jìn)行了自增,可能會(huì)導(dǎo)致后面與expectedModCount不一致 --size; afterNodeRemoval(node); returnnode; } } returnnull; }
上面三種 remove 實(shí)現(xiàn)中,只有第三種 iterator 的 remove 方法在調(diào)用完 removeNode 方法后同步了 expectedModCount 值與 modCount 相同,所以在遍歷下個(gè)元素調(diào)用 nextNode 方法時(shí),iterator 方式不會(huì)拋異常。
到這里是不是有一種恍然大明白的感覺呢!
所以,如果需要對(duì)集合遍歷時(shí)進(jìn)行元素操作需要借助 Iterator 迭代器進(jìn)行,如下:
publicclassHashMapIteratorDemo5{ publicstaticvoidmain(String[]args){ Mapmap=newHashMap>(); map.put(1,"aa"); map.put(2,"bb"); map.put(3,"cc"); Iterator>it=map.entrySet().iterator(); while(it.hasNext()){ Map.Entryentry=it.next(); intkey=entry.getKey(); if(key==1){ it.remove(); } } } }
審核編輯:劉清
-
編譯器
+關(guān)注
關(guān)注
1文章
1623瀏覽量
49108 -
JAVA語(yǔ)言
+關(guān)注
關(guān)注
0文章
138瀏覽量
20090 -
hashmap
+關(guān)注
關(guān)注
0文章
14瀏覽量
2285
原文標(biāo)題:HashMap 為什么不能一邊遍歷一遍刪除
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論