為什么不能在foreach循環(huán)中修改集合证膨?

《阿里巴巴JAVA開發(fā)手冊》中有這樣一條:

不要在 foreach 循環(huán)里進(jìn)行元素的 add / remove 操作盲泛,remove 元素使用 Iterator 方式疗隶。

經(jīng)測試霞扬,當(dāng)在 foreach 循環(huán)中 add / remove 集合元素糕韧,可能會(huì)拋出 ConcurrentModificationException 異常枫振,下面介紹進(jìn)行詳細(xì)說明。

1. foreach循環(huán)

foreach 又稱為增強(qiáng)型for循環(huán)萤彩,通過打印以下代碼的class字節(jié)碼粪滤,我們來了解下其內(nèi)部實(shí)現(xiàn)。

List<String> list = new ArrayList<> ();
list.add("a");
list.add("b");

for (String item : list) {
    System.out.println(item);
}

使用javap -c 命令查看class文件的字節(jié)碼:


image

由上圖紅框圈起的部分不難發(fā)現(xiàn)雀扶,foreach 循環(huán)內(nèi)部實(shí)際是通過 Iterator 實(shí)現(xiàn)的杖小,以上代碼等同于:

List<String> list = new ArrayList<> ();
list.add("a");
list.add("b");

for (Iterator<String> i = list.iterator(); i.hasNext(); ) {
    String item = i.next();
    System.out.println(item);
}

實(shí)際上,foreach 循環(huán)僅是 Java 提供的語法糖愚墓。編譯器隱藏了對(duì) Iterator 的使用予权,使得 foreach 在語法上較傳統(tǒng) for 循環(huán)更加簡潔,也不容易出錯(cuò)浪册。下面我們看下 Iterator.

2. Iterator

Iterator 接口包含以下幾個(gè)主要方法:

boolean hasNext();  // 檢查是否有下個(gè)元素
E next();           // 獲取下個(gè)元素
void remove();      // 移除當(dāng)前指向的元素

在Java集合框架中扫腺,各集合內(nèi)部都實(shí)現(xiàn)了 Iterator 接口,用以對(duì)集合元素進(jìn)行遍歷村象、修改笆环。我們看下 ArrayList 內(nèi)部的 Iterator 實(shí)現(xiàn)。

3. ArrayList$Itr

ArrayList 的內(nèi)部類 Itr 實(shí)現(xiàn)了 Iterator 接口煞肾,Itr 共有3個(gè)成員變量:

 private class Itr implements Iterator<E> {
    int cursor;                      // 下一次遍歷的元素的位置
    int lastRet = -1;                // 前一次返回的元素的位置
    int expectedModCount = modCount;

值得注意的是 expectedModCount 這個(gè)變量咧织,其初始值為 modCount.

3.1 modCount

modCount 是 ArrayList 繼承自 AbstractList 的一個(gè)變量。在AbstractList的源碼注釋中籍救,是這樣解釋這個(gè)變量的:

The number of times this list has been structurally modified. Structural modifications are those that change the size of the list.

翻譯成中文大意為:modCount 為 list 的結(jié)構(gòu)變化次數(shù)习绢,即 list 的元素?cái)?shù)量變化次數(shù)。
查看 ArrayList 的源碼蝙昙,會(huì)發(fā)現(xiàn)在每次調(diào)用 add()remove() 方法闪萄,都會(huì)進(jìn)行 modCount++ 操作。

3.2 expectedModCount

modCount 意為 list 的結(jié)構(gòu)變化次數(shù)奇颠,而 expectedModCount 可被視為 Itr 內(nèi)部記錄的集合結(jié)構(gòu)變化次數(shù)败去,那么該變量有何作用呢?
在 Itr 內(nèi)部有一個(gè) checkForComodification 方法烈拒,如下所示:

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

當(dāng)集合的實(shí)際結(jié)構(gòu)變化次數(shù) 和 Itr 記錄的變化次數(shù)不相等時(shí)圆裕,則拋出文章開頭提到的 ConcurrentModificationException 異常。而在 Itr 的 next() 方法 和 remove() 中都調(diào)用了 checkForComodification 方法荆几。

4. 結(jié)論

由上文吓妆,我們可以得出為何不能在 foreach 循環(huán)中 add/remove 元素的結(jié)論。

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");

for (String item : list) {
    if (item.equals("2")) {
        list.remove(item);
    }
}

相當(dāng)于:

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");

for (Iterator<String> iterator = list.iterator(); iterator.hasNext(); ) {
    String item = iterator.next();
    if (item.equals("2")) {
        list.remove(item);
    }
}
  1. 在進(jìn)行了 2 次 add 元素后吨铸,ArrayList 內(nèi)部的 modCount = 2行拢;
  2. 在進(jìn)行 foreach 循環(huán)時(shí),隱式調(diào)用的 Itr 內(nèi)部 expectedModCount 同樣初始化為 2诞吱;
  3. 在 foreach 循環(huán)中 remove 元素時(shí)舟奠,由于調(diào)用的是 list 的 remove 方法竭缝,會(huì)使 modCount + 1 = 3.
  4. 調(diào)用 Iterator.next() 方法獲取下個(gè)元素前,iterator 檢查到 modCount != expectedModCount沼瘫,拋出 ConcurrentModificationException 異常抬纸。

4.1 為什么使用 Iterator 的 remove 方法操作集合元素不會(huì)有問題?

同樣我們看下 ArrayList 內(nèi)部 Itr 的 remove 方法的源碼:

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);  // 調(diào)用集合的remove()方法
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;    // 更新expectedModCount
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

實(shí)際上晕鹊,調(diào)用 Itr 的 remove() 方法移除集合元素時(shí)松却,首先會(huì)調(diào)用 ArrayList 的 remove() 方法,再對(duì) expectedModCount 進(jìn)行更新溅话。在下次調(diào)用 Itr.next() 方法獲取下個(gè)元素時(shí)晓锻,不會(huì)出現(xiàn) expectedModCount != modCount 的情況。

4.2 Iterator 為什么要檢查集合的結(jié)構(gòu)變化次數(shù)?

這其實(shí)是為了防止多線程并發(fā)修改集合飞几,在一個(gè)線程遍歷集合的同時(shí)砚哆,另一個(gè)線程同時(shí)增刪集合元素,將無法保證數(shù)據(jù)的一致性屑墨,集合的遍歷過程也將被打亂躁锁。采用 modCount 機(jī)制,在此情景下及時(shí)拋出異常卵史,確保同一時(shí)間只會(huì)有一個(gè)線程修改或遍歷集合战转,也即 fail-fast 策略。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末以躯,一起剝皮案震驚了整個(gè)濱河市槐秧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌忧设,老刑警劉巖刁标,帶你破解...
    沈念sama閱讀 218,204評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異址晕,居然都是意外死亡膀懈,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門谨垃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來启搂,“玉大人,你說我怎么就攤上這事刘陶『” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵易核,是天一觀的道長。 經(jīng)常有香客問我浪默,道長牡直,這世上最難降的妖魔是什么缀匕? 我笑而不...
    開封第一講書人閱讀 58,657評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮碰逸,結(jié)果婚禮上乡小,老公的妹妹穿的比我還像新娘。我一直安慰自己饵史,他們只是感情好满钟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著胳喷,像睡著了一般湃番。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吭露,一...
    開封第一講書人閱讀 51,554評(píng)論 1 305
  • 那天吠撮,我揣著相機(jī)與錄音,去河邊找鬼讲竿。 笑死泥兰,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的题禀。 我是一名探鬼主播鞋诗,決...
    沈念sama閱讀 40,302評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼迈嘹!你這毒婦竟也來了削彬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤江锨,失蹤者是張志新(化名)和其女友劉穎吃警,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體啄育,經(jīng)...
    沈念sama閱讀 45,661評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡酌心,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了挑豌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片安券。...
    茶點(diǎn)故事閱讀 39,977評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖氓英,靈堂內(nèi)的尸體忽然破棺而出侯勉,到底是詐尸還是另有隱情,我是刑警寧澤铝阐,帶...
    沈念sama閱讀 35,697評(píng)論 5 347
  • 正文 年R本政府宣布址貌,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏练对。R本人自食惡果不足惜遍蟋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望螟凭。 院中可真熱鬧虚青,春花似錦、人聲如沸螺男。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽下隧。三九已至奢人,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間汪拥,已是汗流浹背达传。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留迫筑,地道東北人宪赶。 一個(gè)月前我還...
    沈念sama閱讀 48,138評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像脯燃,于是被迫代替她去往敵國和親搂妻。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 大家都知道辕棚,不能在ArrayList的For-Each循環(huán)中刪除元素欲主。在Java的入門教程中都會(huì)寫上這條。 可是為...
    南山伐木閱讀 2,691評(píng)論 1 10
  • 傳送門 解讀阿里Java開發(fā)手冊(v1.1.1) - 異常日志 前言 阿里Java開發(fā)手冊談不上圣經(jīng)逝嚎,但確實(shí)是大量...
    kelgon閱讀 4,366評(píng)論 4 50
  • ArrayList是在Java中最常用的集合之一扁瓢,其本質(zhì)上可以當(dāng)做是一個(gè)可擴(kuò)容的數(shù)組,可以添加重復(fù)的數(shù)據(jù)补君,也支持隨...
    ShawnIsACoder閱讀 572評(píng)論 4 7
  • java筆記第一天 == 和 equals ==比較的比較的是兩個(gè)變量的值是否相等引几,對(duì)于引用型變量表示的是兩個(gè)變量...
    jmychou閱讀 1,497評(píng)論 0 3
  • 今天朋友提起‘艷遇’這個(gè)詞于是我就想起了一件事: 去年高考完的暑假整個(gè)像掙脫的野馬 前兩天可算是晝夜顛倒動(dòng)態(tài)狂刷。...
    七友愛栗子閱讀 257評(píng)論 0 1