Android | Java 基礎(chǔ) 為什么在遍歷的時候List不能remove灶似,會報錯ConcurrentModificationException

今天在群里聊天時(摸魚)看見一個問題,為什么遍歷List的時候不能remove瑞你?
啥酪惭?你在逗我嗎?憑什么不能remove者甲,我給你remove一個看看春感。

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        for (int i = 0; i < list.size(); i++) {
            list.remove(i);
        }

run!

Process finished with exit code 0

"for each遍歷"
"..."

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        for (String s: list){
            list.remove(s);
        }
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at practice.ListTest.main(ListTest.java:14)

遇事不決看源碼

原因

眾所周知,for each的本質(zhì)就是Iterator在next()查詢元素,將java文件編譯后的class文件打開即可看到

        List<String> list = new ArrayList();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        Iterator var2 = list.iterator();

        while(var2.hasNext()) {
            String s = (String)var2.next();
            list.remove(s);
        }

然后查看ArrayList.java:851源碼,ArrayList的Iterator 在next()最開始之前進行檢查虏缸,同樣的remove方法也會進行checkForComodification()檢查鲫懒。

  public E next() {
            checkForComodification();
            ...
        }

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

然后打開此方法,當(dāng)modCount 不等于expectedModCount的時候就會拋出該異常,那么這個modCount 和expectedModCount又是什么呢刽辙?

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

先看arraylist的add方法

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

然后點進去

  private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

  private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

這里的方法是判斷arraylist在添加元素的時候是否需要擴容窥岩,在ensureExplicitCapacity方法里找到了modCount++,也就是每次添加元素時就會增加宰缤。接下來看expectedModCount 颂翼。

private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;
        ...
}

我們看到,看arralist的Iterator遍歷器的實例變量里慨灭,expectedModCount 就等于modCount朦乏,也就是說在一開始expectedModCount的數(shù)量等于arralist的數(shù)量,這也就說明了氧骤,在第一次Iterator的next方法里并沒有報錯呻疹,因為modCount = expectedModCount,所以錯誤只能出在第二次next方法里语淘,然后接下來看arraylist的remove方法诲宇。

  public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

  private void fastRemove(int index) {
        modCount++; //此時數(shù)量改變了
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

看到fastRemove方法里的第一行應(yīng)該就清楚原因了际歼,remove的時候modCount增加了,和一開始的expectedModCount 姑蓝,也就是arraylist的一開始的數(shù)量不一致了鹅心,所以會導(dǎo)致ConcurrentModificationException。
所以正確的用法是什么纺荧?

正確用法

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        Iterator iterator = list.iterator();
        while(iterator.hasNext()) {
            String s = (String)iterator.next();
            //if(...)
            iterator.remove();
        }

why?

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

延伸

那么有沒有一種list旭愧,能直接就在遍歷的時候直接進行刪除呢?
答案肯定是有的(聽大佬說的)
CopyOnWriteArrayList

不信宙暇?試試

        List<String> list = new CopyOnWriteArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        for(String s : list){
           list.remove(s);
        }

Process finished with exit code 0

CopyOnWriteArrayList如何做到的输枯?
CopyOnWriteArrayList 類的所有可變操作(add,set等等)都是通過創(chuàng)建底層數(shù)組的新副本來實現(xiàn)的占贫。當(dāng) List 需要被修改的時候桃熄,并不直接修改原有數(shù)組對象,而是對原有數(shù)據(jù)進行一次拷貝型奥,將修改的內(nèi)容寫入副本中瞳收。寫完之后,再將修改完的副本替換成原來的數(shù)據(jù)厢汹,這樣就可以保證寫操作不會影響讀操作了螟深。

從 CopyOnWriteArrayList 的名字可以看出,CopyOnWriteArrayList 是滿足 CopyOnWrite 的 ArrayList烫葬,所謂 CopyOnWrite 的意思:界弧、就是對一塊內(nèi)存進行修改時,不直接在原有內(nèi)存塊中進行寫操作搭综,而是將內(nèi)存拷貝一份垢箕,在新的內(nèi)存中進行寫操作,寫完之后设凹,再將原來指向的內(nèi)存指針指到新的內(nèi)存舰讹,原來的內(nèi)存就可以被回收。

看看它的add方法

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();//獲取當(dāng)前已有的數(shù)組
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);//拷貝一份新的數(shù)組
            newElements[len] = e;//將增加的值放入新的數(shù)組
            setArray(newElements);//把當(dāng)前數(shù)組對象設(shè)置為剛剛拷貝的數(shù)組值
            return true;
        } finally {
            lock.unlock();
        }
    }

remove(object)方法

  public boolean remove(Object o) {
        Object[] snapshot = getArray();
        int index = indexOf(o, snapshot, 0, snapshot.length);//找到當(dāng)前元素的下標(biāo)索引值
        return (index < 0) ? false : remove(o, snapshot, index);
    }

    private boolean remove(Object o, Object[] snapshot, int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();//加鎖
        try {
            Object[] current = getArray();//再次獲取當(dāng)前數(shù)組
            int len = current.length;
            if (snapshot != current) findIndex: {//查找需要移除的元素在數(shù)組里的索引
                int prefix = Math.min(index, len);
                for (int i = 0; i < prefix; i++) {
                    if (current[i] != snapshot[i] && eq(o, current[i])) {
                        index = i;
                        break findIndex;
                    }
                }
                if (index >= len)
                    return false;
                if (current[index] == o)
                    break findIndex;
                index = indexOf(o, current, index, len);
                if (index < 0)
                    return false;
            }
            Object[] newElements = new Object[len - 1];//創(chuàng)建一個新的數(shù)組闪朱,拷貝
            System.arraycopy(current, 0, newElements, 0, index);
            System.arraycopy(current, index + 1,
                             newElements, index,
                             len - index - 1);
            setArray(newElements);//設(shè)置拷貝后的數(shù)組
            return true;
        } finally {
            lock.unlock();//釋放鎖
        }
    }

從add方法和remove方法里不難看出月匣,不管是添加元素還是移除元素,都是通過拷貝數(shù)組并重新賦值來實現(xiàn)的奋姿,所以在遍歷時锄开,remove或者add或者其他一些列操作都不會引起和arraylist一樣的異常的,甚至称诗,你在使用Iterator的時候萍悴,它還會報錯。

private static class COWSubListIterator<E> implements ListIterator<E> {
        ...
        public void remove() {
            throw new UnsupportedOperationException();
        }

        public void set(E e) {
            throw new UnsupportedOperationException();
        }

        public void add(E e) {
            throw new UnsupportedOperationException();
        }
         ...
}


List<String> list = new CopyOnWriteArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        Iterator var2 = list.iterator();

        while(var2.hasNext()) {
            String s = (String)var2.next();
            var2.remove();
        }


Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1176)
    at practice.ListTest.main(ListTest.java:19)

總結(jié)

1.ArrayList在foreach的時候不能直接使用list.remove來操作數(shù)組,因為ArrayList的Iterator 的next方法里每次都會判斷當(dāng)前的數(shù)組的數(shù)量是否和修改后的數(shù)量是否對等癣诱,也就是expectedModCount 和modCount计维,而list.remove方法會修modCount的數(shù)量,所以下一次判斷時就不對等,就會報錯撕予。
2.CopyOnWriteArrayList 可以實現(xiàn)遍歷時直接list.remove,因為CopyOnWriteArrayList 的增刪是通過每次都拷貝一次數(shù)組重新賦值實現(xiàn)的实抡。
3.這個算是java的基礎(chǔ),我居然都不知道吆寨。
4.我是不可回收垃圾赏淌。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末啄清,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子辣卒,更是在濱河造成了極大的恐慌缩擂,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件添寺,死亡現(xiàn)場離奇詭異,居然都是意外死亡懈费,警方通過查閱死者的電腦和手機计露,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來憎乙,“玉大人票罐,你說我怎么就攤上這事∨⒈撸” “怎么了该押?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長阵谚。 經(jīng)常有香客問我蚕礼,道長,這世上最難降的妖魔是什么梢什? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任奠蹬,我火速辦了婚禮,結(jié)果婚禮上嗡午,老公的妹妹穿的比我還像新娘囤躁。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布狸演。 她就那樣靜靜地躺著言蛇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪宵距。 梳的紋絲不亂的頭發(fā)上腊尚,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天,我揣著相機與錄音消玄,去河邊找鬼跟伏。 笑死,一個胖子當(dāng)著我的面吹牛翩瓜,可吹牛的內(nèi)容都是我干的受扳。 我是一名探鬼主播,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼兔跌,長吁一口氣:“原來是場噩夢啊……” “哼勘高!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起坟桅,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤华望,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后仅乓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赖舟,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡夸楣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了豫喧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡讲衫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出涉兽,到底是詐尸還是另有隱情,我是刑警寧澤花椭,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布房午,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏雕蔽。R本人自食惡果不足惜宾娜,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望前塔。 院中可真熱鬧,春花似錦食零、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至考廉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間昌粤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鸵膏,地道東北人。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓谭企,卻偏偏與公主長得像,于是被迫代替她去往敵國和親债查。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,864評論 2 354

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