天天看點

java循環remove List元素報java.util.ConcurrentModificationException 坑:簡單介紹下原因,文章最後有詳細介紹

 坑:

list循環體中,對循環的list進行修改會報ConcurrentModificationException錯誤

今天寫了這麼一段代碼然後報錯

public static void main(String[] args) {
        List <String> list = new ArrayList <> ();
        list.add (null);
        list.add (null);
        list.add ("1");
        for (String string : list) {
            if(StringUtils.isEmpty (string)){
                list.remove (string);
               
            }
        }
        System.out.println (list.size ());
       
    }
           
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at 
           

簡單介紹下原因,文章最後有詳細介紹

因為元素在使用的時候發生了并發的修改,導緻異常抛出。但是删除完畢馬上使用break跳出,則不會觸發報錯。

建議使用下面寫法

    public static void main(String[] args) {
        List <String> list = new ArrayList <> ();
        list.add (null);
        list.add (null);
        list.add ("1");
        Iterator <String> iterator = list.iterator ();
        for (Iterator <String> it = iterator; it.hasNext (); ) {
            String string = it.next ();
            if(StringUtils.isEmpty (string)){
                it.remove (); //注意這裡
            }
        }
        System.out.println (list.size ());
    }

//或者這種寫法(循環方式不同)
public static void main(String[] args) {
        List <String> list = new ArrayList <> ();
        list.add (null);
        list.add (null);
        list.add ("1");
        Iterator <String> iterator = list.iterator ();
        while (iterator.hasNext ()) {
            String string = iterator.next ();
            if (StringUtils.isEmpty (string)) {
                iterator.remove ();
            }
        }

        System.out.println (list.size ());
    }

           

從異常資訊可以發現,異常出現在checkForComodification()方法中。

  我們不忙看checkForComodification()方法的具體實作,我們先根據程式的代碼一步一步看ArrayList源碼的實作:

  首先看ArrayList的iterator()方法的具體實作,檢視源碼發現在ArrayList的源碼中并沒有iterator()這個方法,那麼很顯然這個方法應該是其父類或者實作的接口中的方法,我們在其父類AbstractList中找到了iterator()方法的具體實作,下面是其實作代碼:

public Iterator<E> iterator() {

    return new Itr();

}
           

   從這段代碼可以看出傳回的是一個指向Itr類型對象的引用,我們接着看Itr的具體實作,在AbstractList類中找到了Itr類的具體實作,它是AbstractList的一個成員内部類,下面這段代碼是Itr類的所有實作:

private class Itr implements Iterator<E> {

    int cursor = 0;

    int lastRet = -1;

    int expectedModCount = modCount;

    public boolean hasNext() {

           return cursor != size();

    }

    public E next() {

           checkForComodification();

        try {

        E next = get(cursor);

        lastRet = cursor++;

        return next;

        } catch (IndexOutOfBoundsException e) {

        checkForComodification();

        throw new NoSuchElementException();

        }

    }

    public void remove() {

        if (lastRet == -1)

        throw new IllegalStateException();

           checkForComodification();



        try {

        AbstractList.this.remove(lastRet);

        if (lastRet < cursor)

            cursor--;

        lastRet = -1;

        expectedModCount = modCount;

        } catch (IndexOutOfBoundsException e) {

        throw new ConcurrentModificationException();

        }

    }



    final void checkForComodification() {

        if (modCount != expectedModCount)

        throw new ConcurrentModificationException();

    }

}
           

   首先我們看一下它的幾個成員變量:

  cursor:表示下一個要通路的元素的索引,從next()方法的具體實作就可看出

  lastRet:表示上一個通路的元素的索引

  expectedModCount:表示對ArrayList修改次數的期望值,它的初始值為modCount。

  modCount是AbstractList類中的一個成員變量

protected transient int modCount = 0;
           

  好了,到這裡我們再看看上面的程式:   該值表示對List的修改次數,檢視ArrayList的add()和remove()方法就可以發現,每次調用add()方法或者remove()方法就會對modCount進行加1操作。

  當調用list.iterator()傳回一個Iterator之後,通過Iterator的hashNext()方法判斷是否還有元素未被通路,我們看一下hasNext()方法,hashNext()方法的實作很簡單:

public boolean hasNext() {

    return cursor != size();

}
           

  然後通過Iterator的next()方法擷取到下标為0的元素,我們看一下next()方法的具體實作:   如果下一個通路的元素下标不等于ArrayList的大小,就表示有元素需要通路,這個很容易了解,如果下一個通路元素的下标等于ArrayList的大小,則肯定到達末尾了。

public E next() {

    checkForComodification();

 try {

    E next = get(cursor);

    lastRet = cursor++;

    return next;

 } catch (IndexOutOfBoundsException e) {

    checkForComodification();

    throw new NoSuchElementException();

 }

}
           

  接着往下看,程式中判斷目前元素的值是否為2,若為2,則調用list.remove()方法來删除該元素。   這裡是非常關鍵的地方:首先在next()方法中會調用checkForComodification()方法,然後根據cursor的值擷取到元素,接着将cursor的值賦給lastRet,并對cursor的值進行加1操作。初始時,cursor為0,lastRet為-1,那麼調用一次之後,cursor的值為1,lastRet的值為0。注意此時,modCount為0,expectedModCount也為0。

  我們看一下在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++;

    int numMoved = size - index - 1;

    if (numMoved > 0)

        System.arraycopy(elementData, index+1, elementData, index,

                numMoved);

    elementData[--size] = null; // Let gc do its work

}
           

  那麼注意此時各個變量的值:對于iterator,其expectedModCount為0,cursor的值為1,lastRet的值為0。   通過remove方法删除元素最終是調用的fastRemove()方法,在fastRemove()方法中,首先對modCount進行加1操作(因為對集合修改了一次),然後接下來就是删除元素的操作,最後将size進行減1操作,并将引用置為null以友善垃圾收集器進行回收工作。

  對于list,其modCount為1,size為0。

  接着看程式代碼,執行完删除操作後,繼續while循環,調用hasNext方法()判斷,由于此時cursor為1,而size為0,那麼傳回true,是以繼續執行while循環,然後繼續調用iterator的next()方法:

  注意,此時要注意next()方法中的第一句:checkForComodification()。

  在checkForComodification方法中進行的操作是:

final void checkForComodification() {

    if (modCount != expectedModCount)

    throw new ConcurrentModificationException();

}
           

  很顯然,此時modCount為1,而expectedModCount為0,是以程式就抛出了ConcurrentModificationException異常。   如果modCount不等于expectedModCount,則抛出ConcurrentModificationException異常。

  到這裡,想必大家應該明白為何上述代碼會抛出ConcurrentModificationException異常了。

  關鍵點就在于:調用list.remove()方法導緻modCount和expectedModCount的值不一緻。

繼續閱讀