理解分析java集合操作之ConcurrentModificationException

M莫發表於2018-03-02

相信不少同學在處理List的時候遇到過下面的Exception,

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
	at java.util.ArrayList$Itr.next(ArrayList.java:831)
複製程式碼

話不多說,接下來列舉幾個例子說明問題並且分析其原因。

例一

package main.java.mo.basic;

import java.util.ArrayList;

/**
 * Created by MoXingwang on 2017/7/2.
 */
public class ConcurrentModificationExceptionTest {
    public static void main(String[] args) {
        ArrayList<String> strings = new ArrayList<String>();
        strings.add("a");
        strings.add("b");
        strings.add("c");
        strings.add("d");
        strings.add("e");

        for (String string : strings) {
            if ("e".equals(string)) {
                strings.remove(string);
            }
        }
    }
}
複製程式碼
  • 執行結果
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
	at java.util.ArrayList$Itr.next(ArrayList.java:831)
	at main.java.mo.basic.ConcurrentModificationExceptionTest.main(ConcurrentModificationExceptionTest.java:17)
複製程式碼
  • 分析原因

首先我們知道增強for迴圈其實現原理就是Iterator介面,這一點非常重要,有了個這個知識點 我們才能分析為什麼會出現異常,這個知識點也是最重要最核心的。

根據上面的異常資訊可以看出,異常是從"for (String string : strings) {",這一行拋 出的,這一行怎麼會出錯呢?理解增強for的實現原理了,我們就會知道,執行這一行程式碼的時候 會呼叫Iterator實現類的兩個方法,hasNext()和next(),所以說這個知識點是最重要最核心 的。

先看ArrayList.Iterator的部分原始碼,以及ArrayList.remove(Object o)的部分原始碼

int cursor;       // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;

public boolean hasNext() {
    return cursor != size;
}

@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 (expectedModCount != ArrayList.this.modCount)
     throw new ConcurrentModificationException();
}

複製程式碼
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 remove method that skips bounds checking and does not
 * return the value removed.
 */
private void fastRemove(int index) {
    modCount++;
    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
}
複製程式碼

我們會發現當執行remove(Object o)方法後,ArrayList物件的size減一此時size==4, modCount++了,然後Iterator物件中的cursor==5,hasNext發回了true,導致增強for循 環去尋找下一個元素呼叫next()方法,checkForComodification做校驗的時候,發現modCount 已經和Iterator物件中的expectedModCount不一致,說明ArrayList物件已經被修改過, 為了防止錯誤,丟擲異常ConcurrentModificationException。

回過頭來,再一思考ArrayList的程式碼,讓我們來看看ArrayList本身和內部類Itr,Itr implements Iterator是為了返回給ArrayList.iterator(),在使用的時候可以說他們是 獨立的兩個類,其中各自有兩個重要的屬性;ArrayList中的size、modCount;以及Itr中的 cursor、expectedModCount,理論上他們是同步的,但是我們在某些操作的過程中導致會導致 他們不一致,比如說在這個例子中,我們呼叫的是ArrayList.remove()方法,修改了size和 modCount屬性,但是Itr中的這cursor、expectedModCount卻沒有發生變化,當增強for 迴圈再次執行的時候,呼叫的卻是Itr中的方法,最終發現了資料不一致。這就是本例ConcurrentModificationException 產生的根本原因。

既然問題我們分析清楚了,如何解決呢?這裡我們順著這個思路倒推,列出集中解決辦法。

  • 解決問題

    • 不使用增強for迴圈

    對於這個例子,很明顯我們知道異常的產生原因是由於ArrayList中的屬性和內部類Itr中的 屬性不一致導致的,那麼可以假設在for迴圈和remove操作的時候不設計到Itr類不就得了。 是的,思路很清晰,就這麼簡單。啥都不說先上程式碼。

    ArrayList<String> strings = new ArrayList<String>();
    strings.add("a");
    strings.add("b");
    strings.add("c");
    strings.add("d");
    strings.add("e");
    
    for (int i = 0; i < strings.size(); i++) {
        String element = strings.get(i);
        if("e".equals(element)){
            strings.remove(element);
            i --;//需要自己手動維護索引
        }
    }
    複製程式碼

    使用這種方式處理remove操作,比較尷尬的一點是需要自己手動維護索引,避免漏掉資料。

    • 使用Iterator中的remove方法,不要和ArrayList中的remove方法混著搞

    基於上面的思路,既然不想和Itr有來望,好吧,看來直接使用Itr類中的remove方法, 使用Itr遍歷物件不也是一個好的想法麼。上程式碼。

    ArrayList<String> strings = new ArrayList<String>();
    strings.add("a");
    strings.add("b");
    strings.add("c");
    strings.add("d");
    strings.add("e");
    
    Iterator<String> iterator = strings.iterator();
    while (iterator.hasNext()){
        String element = iterator.next();
        if("e".equals(element)){
            iterator.remove();
        }
    }
    複製程式碼
    • 刪除元素的時候不再遍歷了,直接removeAll 既然異常是對list做遍歷和remove操作的時候出現的,好吧,暴力點,我能不遍歷的時候做remove操作嗎? 好吧,思路正確,滿足你。
    ArrayList<String> strings = new ArrayList<String>();
    strings.add("a");
    strings.add("b");
    strings.add("c");
    strings.add("d");
    strings.add("e");
    
    ArrayList<String> tempStrings = new ArrayList<String>();
    for (String string : strings) {
        if("e".equals(string)){
            tempStrings.add(string);
        }
    }
    strings.removeAll(tempStrings);
    複製程式碼
    • 其它方法 思路總是多的,比如說加個鎖保證資料正確,什麼去掉這麼到校驗自己實現個ArrayList, 怎麼地都行,你想怎麼玩就怎麼玩,方便點的話直接使用java.util.concurrent包下面的CopyOnWriteArrayList。 方法很多,怎麼開心就好。

例二

說完例一說例二,剛剛是ArrayList,現在試試LinkedList。

package main.java.mo.basic;

import java.util.LinkedList;

/**
 * Created by MoXingwang on 2017/7/2.
 */
public class ConcurrentModificationExceptionTest {
    public static void main(String[] args) {
        LinkedList<String> strings = new LinkedList<String>();
        strings.add("a");
        strings.add("b");
        strings.add("c");
        strings.add("d");
        strings.add("e");

        for (String string : strings) {
            if ("e".equals(string)) {
                strings.remove(string);
            }
        }
    }
}
複製程式碼

這段程式碼和例一的沒啥區別,唯一不同的就是ArrayList換成了LinkedList,突然發現執行這段程式碼怎麼就不報錯了呢。 這不是搞事情麼?好吧,再上一段程式碼。

package main.java.mo.basic;

import java.util.LinkedList;

/**
 * Created by MoXingwang on 2017/7/2.
 */
public class ConcurrentModificationExceptionTest {
    public static void main(String[] args) {
        LinkedList<String> strings = new LinkedList<String>();
        strings.add("a");
        strings.add("b");
        strings.add("c");
        strings.add("d");
        strings.add("e");
        strings.add("f");
        strings.add("g");

        for (String string : strings) {
            if ("e".equals(string)) {
                strings.remove(string);
            }
        }
    }
}
複製程式碼

再執行一下這一段程式碼,返回結果居然是這樣:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.LinkedList$ListItr.checkForComodification(LinkedList.java:953)
	at java.util.LinkedList$ListItr.next(LinkedList.java:886)
	at main.java.mo.basic.ConcurrentModificationExceptionTest.main(ConcurrentModificationExceptionTest.java:19)
複製程式碼

仔細一看才發現strings裡面多了兩個元素,怎麼差別就這麼大呢,分析方法和例一完全一樣, 想必按照例子一的分析一定非常簡單的找到答案,這就就不舉例子了。

總結

總得來說,本文雖讓沒有對ConcurrentModificationException發生的情況深入涉及, 但是理解方法和思路都是一樣的,文章中的兩個例子告訴我們, 當在處理Iterable的實現類做元素remove操作,並且是在for迴圈中處理的時候, 理解了這些東西就會避免掉bug以及出現錯誤。

相關文章