理解分析java集合操做之ConcurrentModificationException

相信很多同窗在處理List的時候遇到過下面的Exception,java

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)
複製代碼

話很少說,接下來列舉幾個例子說明問題而且分析其緣由。bash

例一

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接口,這一點很是重要,有了個這個知識點 咱們才能分析爲何會出現異常,這個知識點也是最重要最核心的。ui

根據上面的異常信息能夠看出,異常是從"for (String string : strings) {",這一行拋 出的,這一行怎麼會出錯呢?理解加強for的實現原理了,咱們就會知道,執行這一行代碼的時候 會調用Iterator實現類的兩個方法,hasNext()和next(),因此說這個知識點是最重要最核心 的。this

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

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 產生的根本緣由。code

既然問題咱們分析清楚了,如何解決呢?這裏咱們順着這個思路倒推,列出集中解決辦法。對象

  • 解決問題索引

    • 不使用加強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以及出現錯誤。

相關文章
相關標籤/搜索