List遍歷時刪除與迭代器(Iterator)解惑


List集合使我們非常熟悉的,ArrayList等集合為我們提供了remove()方法,但在遍歷時卻不能隨便使用,我們我們今天便從實現層面討論下原因以及Iterator的相關知識。

ArrayList 遍歷時刪除方法

for循環向后遍歷的陷阱

for(int i=0;i<list.size();i++){
    if(list.get(i).equals("del"))
        list.remove(i);
}

從前向后for循環遍歷同時如果調用ArrayList提供的remove方法的話主要你刪除第一個元素后會導致后面的元素向前移動,比如你刪除了第0個元素后后面的n-1個元素都向前移動一個位置,但是i的值變為了1,而實際上一開始位於index=1位置的元素已經被移動到了index=0位置上,導致漏掉部分元素。

解決辦法

從list最后1個元素開始從后向前遍歷。

for(int i=list.size()-1;i>=0;i--){
    if(list.get(i).equals("del"))
        list.remove(i);

增強型for循環(foreach)遇到的問題

for(String s:list){  
    if(s.equals("two")){  
        list.remove(s);  
    }  
}  

如上代碼運行會報錯如下

Exception in thread "main" java.util.ConcurrentModificationException  
    at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)  
    at java.util.AbstractList$Itr.next(AbstractList.java:343)  
    at Test.main(Test.java:22)  

為什么會突然報錯?我們先考慮一個問題,什么是foreach?

通過對class文件反編譯,我們可以發現對於List集合,foreach實際上是調用了itearator()方式通過迭代器進行遍歷。那思路就清晰了,我們來看一看ArrayList實現的Itr迭代器的next()方法源碼

// ArrayList.java#Itr

public E next() {
    // 校驗是否數組發生了變化
    checkForComodification();
    // 判斷如果超過 size 范圍,拋出 NoSuchElementException 異常
    int i = cursor; // <1> i 記錄當前 cursor 的位置
    if (i >= size)
        throw new NoSuchElementException();
    // 判斷如果超過 elementData 大小,說明可能被修改了,拋出 ConcurrentModificationException 異常
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    // <2> cursor 指向下一個位置
    cursor = i + 1;
    // <3> 返回當前位置的元素
    return (E) elementData[lastRet = i]; // <4> 此處,會將 lastRet 指向當前位置
}

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

我們在這發現了拋出的異常,也看到了拋出異常的原因modCount != expectedModCount,這個modCount和expectedModCount是怎么回事呢,我們先來看看expectedModCount

// ArrayList.java#Itr

/**
 * 下一個訪問元素的位置,從下標 0 開始。
 */
int cursor;       // index of next element to return
/**
 * 上一次訪問元素的位置。
 *
 * 1. 初始化為 -1 ,表示無上一個訪問的元素
 * 2. 遍歷到下一個元素時,lastRet 會指向當前元素,而 cursor 會指向下一個元素。這樣,如果我們要實現 remove 方法,移除當前元素,就可以實現了。
 * 3. 移除元素時,設置為 -1 ,表示最后訪問的元素不存在了,都被移除咧。
 */
int lastRet = -1; // index of last element returned; -1 if no such
/**
 * 創建迭代器時,數組修改次數。
 *
 * 在迭代過程中,如果數組發生了變化,會拋出 ConcurrentModificationException 異常。
 */
int expectedModCount = modCount;

// prevent creating a synthetic constructor
Itr() {}

從源碼中我們可以知道,expectedModCount是Itr的1個屬性,記錄創建迭代器時數組的修改次數。

我們再來看看modCount又是在哪發生變化的呢?

// ArrayList.java
public E remove(int index) {
    // 校驗 index 不要超過 size
    Objects.checkIndex(index, size);
    final Object[] es = elementData;

    // 記錄該位置的原值
    @SuppressWarnings("unchecked") E oldValue = (E) es[index];
    // <X>快速移除
    fastRemove(es, index);

    // 返回該位置的原值
    return oldValue;
}

private void fastRemove(Object[] es, int i) {
    // 增加數組修改次數
    modCount++;
    // <Y>如果 i 不是移除最末尾的元素,則將 i + 1 位置的數組往前挪
    final int newSize;
    if ((newSize = size - 1) > i) // -1 的原因是,size 是從 1 開始,而數組下標是從 0 開始。
        System.arraycopy(es, i + 1, es, i, newSize - i);
    // 將新的末尾置為 null ,幫助 GC
    es[size = newSize] = null;
}

至此我們就明白了,ArrayList在進行增加/刪除操作時會對modCount進行修改,記錄修改次數,這本沒什么問題,但使用itearator遍歷時會進行checkForComodification()操作,從而導致modCount != expectedModCount拋出ConcurrentModificationException。

整體來說,也就是Iterator遍歷時不允許並發調用ArrayList的remove/add操作進行修改,否則會拋出異常。

那我們應該怎樣在遍歷時進行增改操作呢?

使用迭代器進行遍歷同時修改操作

Iterator<String> it = list.iterator();
while(it.hasNext()){
    String x = it.next();
    if(x.equals("del")){
        it.remove();
    }
}

如此我們便可以正常的循環及刪除。可能有同學還會有疑慮為什么這樣不會拋出剛才的異常呢?我們仍然可以從Itr類的remove()方法源碼中找到答案。

// ArrayList.java#Itr

public void remove() {
    // 如果 lastRet 小於 0 ,說明沒有指向任何元素,拋出 IllegalStateException 異常
    if (lastRet < 0)
        throw new IllegalStateException();
    // 校驗是否數組發生了變化
    checkForComodification();

    try {
        // <1> 移除 lastRet 位置的元素
        ArrayList.this.remove(lastRet);
        // <2> cursor 指向 lastRet 位置,因為被移了,所以需要后退下
        cursor = lastRet;
        // <3> lastRet 標記為 -1 ,因為當前元素被移除了
        lastRet = -1;
        // <4> 記錄新的數組的修改次數
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

原來,Itr提供的remove方法也是調用了ArrayList的remove方法,但是他在調用之后還修改了expectedModCount的值,這樣就可以在遍歷過程中分清修改操作的“敵我”啦。

iterator調用remove()方法為什么要先調用next()方法?

這也是在使用iterator時可能會讓部分同學感到困惑的問題。我們看一看上文中的Itr#remove()方法可以發現,它實際上是刪除lastRet指向的元素,而lastRet在每次remove調用后會默認置為-1,並將cursor指針向前走一個位置(因為由於刪除元素接下來要把被刪除元素后面的所有數組元素向前挪一個位置)。接下來我們再看next()方法源碼,next調用后會將lastRet指向上個元素的索引,cursor指向下一個位置,所以調用remove()方法要先調用next()方法,注意,next方法返回值為上一個元素的值。

所以我們可以總結下,next方法返回的為上一個元素的值,remove刪的也是上一個元素。cursur指向的是后一個元素,在發生remove后,cursor會回退一個位置從而保證遍歷不漏元素。因此,也就不難理解hasNext()方法的實現邏輯了。

// ArrayList.java#Itr

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

源碼看完了,我們再回過頭來看看概念,相信就好理解多啦。

快速失敗(fail-fast)

對於ArrayList,HashMap 這些不是線程安全的集合類,如果在使用迭代器的過程中有其他線程修改了map,那么將拋出ConcurrentModificationException,這就是所謂fail-fast策略。這一策略在源碼中的實現是通過 modCount 域,modCount 顧名思義就是修改次數,對集合內容的修改都將增加這個值,那么在迭代器初始化過程中會將這個值賦給迭代器的 expectedModCount。在迭代過程中,判斷 modCount 跟 expectedModCount 是否相等,如果不相等就表示已經有其他線程修改了 Map:注意到 modCount 聲明為 volatile,保證線程之間修改的可見性。

場景:java.util包下的集合類都是快速失敗的,不能在多線程下發生並發修改(迭代過程中被修改)。

安全失敗(fail-safe)

采用安全失敗機制的集合容器,在遍歷時不是直接在集合內容上訪問的,而是先復制原有集合內容,在拷貝的集合上進行遍歷。
原理:由於迭代時是對原集合的拷貝進行遍歷,所以在遍歷過程中對原集合所作的修改並不能被迭代器檢測到,所以不會觸發Concurrent Modification Exception。
缺點:基於拷貝內容的優點是避免了Concurrent Modification Exception,但同樣地,迭代器並不能訪問到修改后的內容,即:迭代器遍歷的是開始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發生的修改迭代器是不知道的。
場景:java.util.concurrent包下的容器都是安全失敗,可以在多線程下並發使用,並發修改。

參考資料

Java快速失敗(fail-fast)和安全失敗(fail-safe)區別


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM