RM新时代网站-首页

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

HashMap遍歷操作為什么不能一邊遍歷一遍刪除呢?

jf_ro2CN3Fa ? 來源:稀土掘金 ? 2023-02-10 11:25 ? 次閱讀

前段時(shí)間,同事在代碼中 KW 掃描的時(shí)候出現(xiàn)這樣一條:

ad61d7c6-a8d8-11ed-bfe3-dac502259ad0.png

上面出現(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ù)部分):

ad78b9fa-a8d8-11ed-bfe3-dac502259ad0.png

也許我們不能很清楚這些指令到底有什么作用,但是我們可以對(duì)比一下下面段代碼產(chǎn)生的字節(jié)碼指令:

publicclassHashMapIteratorDemo2{
String[]arr={
"aa",
"bb",
"cc"
};

publicvoidtest1(){
for(inti=0;i
ad8ca438-a8d8-11ed-bfe3-dac502259ad0.png

看看兩個(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ì)比如下:

ad9fa5e2-a8d8-11ed-bfe3-dac502259ad0.pngadb73e6e-a8d8-11ed-bfe3-dac502259ad0.png

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

輸出:

adcdbe00-a8d8-11ed-bfe3-dac502259ad0.png

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é)果:

ade3cc72-a8d8-11ed-bfe3-dac502259ad0.png

執(zhí)行沒有問題,put 操作也成功了。

但是!但是!但是!問題來了!?。?/p>

我們知道 HashMap 是一個(gè)線程不安全的集合類,如果使用 foreach 遍歷時(shí),進(jìn)行add, remove 操作會(huì) java.util.ConcurrentModificationException 異常。put 操作可能會(huì)拋出該異常。(為什么說可能,這個(gè)我們后面解釋)

為什么會(huì)拋出這個(gè)異常呢?

我們先去看一下 Java API 文檔對(duì) HasMap 操作的解釋吧。

adf7e900-a8d8-11ed-bfe3-dac502259ad0.png

翻譯過來大致的意思就是:該方法是返回此映射中包含的鍵的集合視圖。

集合由映射支持,如果在對(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)異常:

ae0aa3ec-a8d8-11ed-bfe3-dac502259ad0.png

這就是驗(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):

ae228c78-a8d8-11ed-bfe3-dac502259ad0.png

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






審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 編譯器
    +關(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)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    為什么L298n的輸出端本來是一邊一邊低的,

    為什么L298n的輸出端本來是一邊一邊低的,接上直流電機(jī)后,兩端的電壓就變了,就在跳動(dòng),0到4.3之間跳動(dòng):
    發(fā)表于 08-09 14:53

    手機(jī)一邊充電一邊使用對(duì)人,手機(jī),電池有哪些危害

    手機(jī)一邊充電一邊使用對(duì)人,手機(jī),電池有哪些危害
    發(fā)表于 08-04 09:43

    手機(jī)如何實(shí)現(xiàn)一邊充電一邊聽歌(聽)

    ,如何實(shí)現(xiàn)一邊充電一邊聽歌(聽)?因此,USB Type-C接口的轉(zhuǎn)接器應(yīng)時(shí)而生了,輕松的實(shí)現(xiàn)不同音頻插頭與音響耳機(jī)之間的相互轉(zhuǎn)換,
    發(fā)表于 09-14 08:41

    如何通過media graph遍歷entity?

    如何通過media graph遍歷entity?
    發(fā)表于 03-10 07:04

    Merkle樹遍歷技術(shù)的研究

    Merkle樹應(yīng)用于數(shù)字加密技術(shù)。它的遍歷技術(shù)主要包含樹的根節(jié)點(diǎn)生成和認(rèn)證路徑的生成。本文主要比較各種Merkle樹的遍歷技術(shù),提出自己的遍歷方法,并進(jìn)行了實(shí)驗(yàn)仿真和對(duì)實(shí)驗(yàn)結(jié)果的
    發(fā)表于 03-01 16:16 ?14次下載

    二叉樹的前序遍歷、中序遍歷、后續(xù)遍歷的非遞歸實(shí)現(xiàn)

    前序遍歷:先訪問該節(jié)點(diǎn),然后訪問該節(jié)點(diǎn)的左子樹和右子樹; 中序遍歷:先訪問該節(jié)點(diǎn)的左子樹,然后訪問該節(jié)點(diǎn),再訪問該節(jié)點(diǎn)的右子樹; 后序遍歷:想訪問該節(jié)點(diǎn)的左子樹和右子樹,然后訪問該節(jié)點(diǎn)。
    發(fā)表于 11-27 11:24 ?1133次閱讀

    jquery的each遍歷方法

    本文為大家介紹Jquery中each的三種遍歷方法,有興趣的伙伴可以參考下。
    發(fā)表于 12-03 10:19 ?2556次閱讀

    圖不同存儲(chǔ)方式的應(yīng)用和遍歷操作及綜合應(yīng)用資料說明

    、 實(shí)驗(yàn)?zāi)康模?.掌握?qǐng)D的不同存儲(chǔ)方式的應(yīng)用;2. 掌握?qǐng)D的遍歷操作。 二、 實(shí)驗(yàn)要求:1.用C語(yǔ)言實(shí)現(xiàn)源程序的編寫;2.源程序應(yīng)書寫規(guī)范,要采用縮進(jìn)格式,適當(dāng)添加注釋;3.程序要具有
    發(fā)表于 03-11 08:00 ?4次下載
    圖不同存儲(chǔ)方式的應(yīng)用和<b class='flag-5'>遍歷</b><b class='flag-5'>操作</b>及綜合應(yīng)用資料說明

    螺旋遍歷二維數(shù)組漫畫講解

    來自公眾號(hào):程序員小灰 第二天 什么意思?我們來舉個(gè)例子,給定下面這樣個(gè)二維數(shù)組: 我們需要從左上角的元素1開始,按照順時(shí)針進(jìn)行螺旋遍歷,
    的頭像 發(fā)表于 11-26 14:01 ?1748次閱讀

    Java的iterator和foreach遍歷集合源代碼

    Java的iterator和foreach遍歷集合源代碼
    發(fā)表于 03-17 09:16 ?9次下載
    Java的iterator和foreach<b class='flag-5'>遍歷</b>集合源代碼

    二叉樹的前序遍歷非遞歸實(shí)現(xiàn)

    我們之前說了二叉樹基礎(chǔ)及二叉的幾種遍歷方式及練習(xí)題,今天我們來看下二叉樹的前序遍歷非遞歸實(shí)現(xiàn)。 前序遍歷的順序是, 對(duì)于樹中的某節(jié)點(diǎn),先遍歷
    的頭像 發(fā)表于 05-28 13:59 ?1952次閱讀

    總結(jié)下OpenCV遍歷圖像的幾種方法

    在圖形處理中,遍歷每個(gè)像素點(diǎn)是最基本的功能,是做算法的基礎(chǔ),這篇文章來總結(jié)下OpenCV遍歷圖像的幾種方法。
    的頭像 發(fā)表于 01-18 15:08 ?1724次閱讀

    為什么很少有人按列去遍歷訪問二維數(shù)組?

    二維數(shù)組大家都很熟悉,正常人遍歷二維數(shù)組都是行來的,為什么很少有人按列去遍歷?
    的頭像 發(fā)表于 02-12 09:47 ?711次閱讀
    為什么很少有人按列去<b class='flag-5'>遍歷</b>訪問二維數(shù)組<b class='flag-5'>呢</b>?

    如何遍歷中文字符串

    今天和大家分享下如何遍歷中文字符串,主要是如何打印中文字符,因?yàn)橹形淖址總€(gè)字符占用不只個(gè)字節(jié)的空間,如果我們逐個(gè)字節(jié)遍歷,會(huì)出現(xiàn)奇怪的結(jié)果。而UTF-8編碼寫的中文字符是有特定結(jié)構(gòu)的,我們可以
    的頭像 發(fā)表于 07-03 09:15 ?693次閱讀
    如何<b class='flag-5'>遍歷</b>中文字符串

    python如何遍歷列表并提取

    遍歷列表是Python中非常常見的操作,可以使用for循環(huán)或者while循環(huán)來實(shí)現(xiàn)。下面我將詳細(xì)介紹如何使用for循環(huán)遍歷列表并提取元素。 首先,讓我們簡(jiǎn)單了解
    的頭像 發(fā)表于 11-23 15:55 ?1368次閱讀
    RM新时代网站-首页