為什么阿里巴巴Java開發(fā)手冊中強制要求不要在foreach循環(huán)里進行元素的remove和add操作坯屿?

在閱讀《阿里巴巴Java開發(fā)手冊》時,發(fā)現(xiàn)有一條關(guān)于在 foreach 循環(huán)里進行元素的 remove/add 操作的規(guī)約乏德,具體內(nèi)容如下:

阿里巴巴Java開發(fā)手冊

錯誤演示

我們首先在 IDEA 中編寫一個在 foreach 循環(huán)里進行 remove 操作的代碼:

import java.util.ArrayList;
import java.util.List;

public class ForEachTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("wupx");
        list.add("love");
        list.add("huxy");
        for (String temp : list) {
            if ("love".equals(temp)) {
                list.remove(temp);
            }
        }
        System.out.println(list);
    }
}

此時執(zhí)行代碼喊括,編譯正確瘾晃,執(zhí)行成功!輸出 [wupx, huxy]蹦误。

接著我們把 “l(fā)ove” 換成 “wupx” 或是 “huxy” 再來運行下,執(zhí)行結(jié)果如下:

納尼舱沧,居然報錯了偶洋,為什么第一次運行沒有報錯呢?讓我們一起來進行探討吧牵寺!

追根溯源

為了研究為什么會出現(xiàn)這樣的情況恩脂,我們可以根據(jù)異常堆棧信息,去追蹤錯誤黎休,其中涉及到的部分源碼如下:

private class Itr implements Iterator<E> {
    int cursor;       // 下一個要返回的元素的索引
    int lastRet = -1; // 返回的最后一個元素的索引(如果沒有返回-1)
    int expectedModCount = modCount;

    public boolean hasNext() {
        return cursor != size;
    }
    
    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        Objects.requireNonNull(consumer);
        final int size = ArrayList.this.size;
        int i = cursor;
        if (i >= size) {
            return;
        }
        final Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        while (i != size && modCount == expectedModCount) {
            consumer.accept((E) elementData[i++]);
        }
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }
    
    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

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

從代碼中可以看出势腮,其實在集合遍歷時維護一個初始值為 0 的游標(biāo) cursor捎拯,從頭到尾地進行掃描,在 cursor==size 時玄渗,退出遍歷狸眼。如下圖所示,執(zhí)行 remove 這個元素后岁钓,所有元素往前拷貝微王, size=size-1 即為2 ,這時 cursor 也等于 2钧大。在執(zhí)行
hasNext() 時罩旋, 結(jié)果為 false 眶诈,退出循環(huán)體瓜饥,并沒有機會執(zhí)行到 next() 的第一行代碼
checkForComodification() ,此方法用來判斷 expectedModCount 和 modCount 是否相等宪潮,
如果不相等趣苏,則拋出 ConcurrentModificationException 異常。

集合的cursor與size

之所以會報 ConcurrentModificationException 異常谣光,是因為觸發(fā)了 Java 的 fail-fast 機制,該機制是集合中比較常見的錯誤檢測機制蟀悦,通常出現(xiàn)在遍歷集合元素的過程中。舉個生活中的栗子:

比如上體育課時日戈,在上課前都會依次報數(shù),如果在報數(shù)期間份氧,有人突然加進來弯屈,還要重新報數(shù),再次報數(shù)厅缺,又有同學(xué)溜出去了宴偿,又要重新報數(shù),這就是 fail-fast 機制窄刘,它是對集合(班級同學(xué))遍歷操作的錯誤檢測機制,在遍歷中途出現(xiàn)意料之外的修改時活翩,通過 unchecked 異常反饋出來。這種機制經(jīng)常出現(xiàn)在多線程環(huán)境下纱新,當(dāng)前線程會維護一個計數(shù)比較器(expectedModCount),記錄已經(jīng)修改的次數(shù)遇汞。在進入遍歷前簿废,會把實時修改次數(shù)
modCount 賦值給 expectedModCount,如果這兩個數(shù)據(jù)不相等歪赢,則拋出異常单料。java.util 下的所有集合類都是 fail-fast。

不二法門

既然在 foreach 循環(huán)里進行元素的 remove/add 操作會有問題白对,那么我們可以使用手冊中推薦的 Iterator 機制進行遍歷時的刪除或新增换怖,代碼如下:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ForEachTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("wupx");
        list.add("love");
        list.add("huxy");

        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            if (iterator.next().equals("wupx")) {
                iterator.remove();
            }
        }
        System.out.println(list);
    }
}

如果是多線程并發(fā),還需要在 Iterator 遍歷時加鎖条摸,或者使用并發(fā)容器 CopyOnWriteArrayList 代替 ArrayList铸屉,該容器內(nèi)部會對 Iterator 進行加鎖操作。

總結(jié)

本文針對《阿里巴巴Java開發(fā)手冊》中的強制要求不要在 foreach 循環(huán)里進行元素的 remove/add 操作出發(fā)子巾,從源碼層面來解釋為什么小压,還用生活中的栗子來介紹 Java 中的 fail-fast 機制,因此在進行元素的 remove/add 操作時要用 Iterator 去遍歷刪除或新增怠益。

參考

《Java開發(fā)手冊》華山版

《碼出高效:Java開發(fā)手冊》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蜻牢,一起剝皮案震驚了整個濱河市偏陪,隨后出現(xiàn)的幾起案子笛谦,更是在濱河造成了極大的恐慌,老刑警劉巖饥脑,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灶轰,死亡現(xiàn)場離奇詭異刷钢,居然都是意外死亡,警方通過查閱死者的電腦和手機内地,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門阱缓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事并蝗。” “怎么了滚停?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵键畴,是天一觀的道長。 經(jīng)常有香客問我起惕,道長,這世上最難降的妖魔是什么问词? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任激挪,我火速辦了婚禮辰狡,結(jié)果婚禮上宛篇,老公的妹妹穿的比我還像新娘薄湿。我一直安慰自己,他們只是感情好嘿般,可當(dāng)我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布炉奴。 她就那樣靜靜地躺著,像睡著了一般赛糟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上璧南,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天司倚,我揣著相機與錄音篓像,去河邊找鬼。 笑死员辩,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的丹皱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼摊崭,長吁一口氣:“原來是場噩夢啊……” “哼爽室!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起阔墩,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤啸箫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后忘苛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡召川,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年荧呐,在試婚紗的時候發(fā)現(xiàn)自己被綠了纸镊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡峰搪,死狀恐怖凯旭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情罐呼,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站矫膨,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏危尿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一谊娇、第九天 我趴在偏房一處隱蔽的房頂上張望罗晕。 院中可真熱鬧赠堵,春花似錦法褥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至切距,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間饵沧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工狼牺, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留礼患,地道東北人。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓悄泥,卻偏偏與公主長得像肤粱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子领曼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,941評論 2 355

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