首頁 > 軟體

為什麼在foreach迴圈中JAVA集合不能新增或刪除元素

2021-06-11 19:00:22

1. 編碼強制規約

在《阿里巴巴Java開發手冊》中,針對集合操作,有一項規定,如下:

【強制】不要在 foreach 迴圈裡進行元素的 remove/add 操作。remove 元素請使用 Iterator方式,如果並行操作,需要對 Iterator 物件加鎖。

public class SimpleTest {
    public static void main(String[] args) {
        List<String> list = Lists.newArrayList();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
 
        //正例
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String item = iterator.next();
            if ("1".equalsIgnoreCase(item)) {
                iterator.remove();
            }
        }
 
        //反例
        for (String item : list) {
            if ("2".equals(item)) {
                list.remove(item);
            }
        }
    }
}

2. 原因分析

在迴圈或迭代時,會首先建立一個迭代範例,這個迭代範例的expectedModCount 賦值為集合的modCount.   

每當迭代器使⽤ hashNext() / next() 遍歷下⼀個元素之前,都會檢測 modCount 變數與expectedModCount 值是否相等,相等的話就返回遍歷;否則就丟擲異常【ConcurrentModificationException】,終⽌遍歷

如果在迴圈中新增或刪除元素,是直接呼叫集合的add,remove方法【導致了modCount增加或減少】,但這些方法不會修改迭代範例中的expectedModCount,導致在迭代範例中expectedModCount 與 modCount的值不相等,丟擲ConcurrentModificationException異常

但迭代器中的remove,add方法,會在呼叫集合的remove,add方法後,將expectedModCount 重新賦值為modCount,所以在迭代器中增加、刪除元素是可以正常執行的。

可以參考ArrayList中的內部私有類Itr、ListItr的原始碼

public Iterator<E> iterator() {
        return new Itr();
    }
 
/**
     * An optimized version of AbstractList.Itr
     */
    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;
 
        Itr() {}
 
        //刪除了一些程式碼
 
        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();
            }
        }
 
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
 
  }
   public E remove(int index) {
        rangeCheck(index);
 
        modCount++;
        E oldValue = elementData(index);
 
        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
 
        return oldValue;
    }

3. 相關知識介紹

3.1. 什麼是快速失敗(fail-fast)?

快速失敗(fail-fast) 是 Java 集合的⼀種錯誤檢測機制。在使⽤迭代器對集合進⾏遍歷的時候,在多執行緒下操作⾮安全失敗(fail-safe)的集合類可能就會觸發 fail-fast 機制,導致丟擲ConcurrentModificationException 異常。 

另外,在單執行緒下,如果在遍歷過程中對集合物件的內容進⾏了修改的話也會觸發 fail-fast 機制。

舉個例⼦:多執行緒下,如果執行緒 1 正在對集合進⾏遍歷,此時執行緒 2 對集合進⾏修改(增加、刪除、修改),或者執行緒 1 在遍歷過程中對集合進⾏修改,都會導致執行緒 1 丟擲ConcurrentModificationException 異常。

3.2. 什麼是安全失敗(fail-safe)呢?

採⽤安全失敗機制的集合容器,在遍歷時不是直接在集合內容上存取的,⽽是先複製原有集合內容,在拷⻉的集合上進⾏遍歷。所以,在遍歷過程中對原集合所作的修改並不能被迭代器檢測到,故不會拋ConcurrentModificationException 異常。

到此這篇關於為什麼在foreach迴圈中JAVA集合不能新增或刪除元素的文章就介紹到這了,更多相關JAVA集合新增或刪除元素內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com