foreach迴圈中為什麼不要進行remove/add操作

Google愛喝茶發表於2018-08-07

先來看一段程式碼,摘自阿里巴巴的java開發手冊

List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
for (String temp : a) {
  if("1".equals(temp)){
      a.remove(temp);
} 
}
複製程式碼

此時執行程式碼,沒有問題,但是需要注意,迴圈此時只執行了一次。具體過程後面去分析。 再來看一段會出問題的程式碼

List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
for (String temp : a) {
 if("2".equals(temp)){
     a.remove(temp);
} 
}
複製程式碼

輸出為:Exception in thread "main" java.util.ConcurrentModificationException 是不是很奇怪?接下來將class檔案,反編譯下,結果如下

List a = new ArrayList();
a.add("1");
a.add("2");
Iterator i$ = a.iterator();
do
{
  if(!i$.hasNext())
      break;
  String temp = (String)i$.next();
 if("1".equals(temp))
     a.remove(temp);
} while(true);
複製程式碼

幾個需要注意的點:

  1. foreach遍歷集合,實際上內部使用的是iterator。
  2. 程式碼先判斷是否hasNext,然後再去呼叫next,這兩個函式是引起問題的關鍵。
  3. 這裡的remove還是list的remove方法。

先去觀察下list.remove()方法中的核心方法fastRemove()方法。

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
 }
複製程式碼

注意下,modCount++,此處先不表,下文再說這個引數。
順路觀察下list.add()方法

public boolean add(E e) {
     ensureCapacityInternal(size + 1);  // Increments modCount!!
     elementData[size++] = e;
     return true;
 }
複製程式碼

注意第二行的註釋,說明這個方法也會使modCount++ 再去觀察下,iterator()方法

public Iterator<E> iterator() {
     return new 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;

      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];
     }

     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();
     }
 }
複製程式碼

幾個需要注意的點:

  1. 在iterator初始化的時候(也就是for迴圈開始處),expectedModCount = modCount,猜測是和當時list內部的元素數量有關係(已證實)。
  2. 當cursor != size的時候,hasNext返回true
  3. next()函式的第一行,checkForComodification()這個函式就是報錯的原因 這個函式就是萬惡之源
  4. 第39行,mod != expectedModCount 就會丟擲ConcurrentModificationException()

接下來分析文章開頭的第一個例子,為啥不會報錯? 第一個例子執行完第一次迴圈後,mod = 3 expectedModCount =2 cursor = 1 size = 1 所以程式在執行hasNext()的時候會返回false,所以程式不會報錯。 第二個例子執行完第二次迴圈後,mod = 3 expectdModCount = 2 cursor = 2 size = 1 此時cursor != size 程式認定還有元素,繼續執行迴圈,呼叫next方法但是此時mod != expectedModCount 所以此時會報錯。 道理我們都懂了,再看一個例子

public static void main(String[] args) throws Exception {
      List<String> a = new ArrayList<String>();
      a.add("1");
      a.add("2");
      for (String temp : a) {
          System.out.println(temp);
          if("2".equals(temp)){
              a.add("3");
              a.remove("2");
         } 
     }
}
複製程式碼

此時輸出為: 1 2 顯然,程式並沒有執行第三次迴圈,第二次迴圈結束,cursor再一次等於size,程式退出迴圈。 與remove類似,將文章開頭的程式碼中remove替換為add,我們會發現無論是第一個例子還是第二個例子,都會丟擲ConcurrentModificationException錯誤。 原因同上,程式碼略。


手冊上推薦的程式碼如下

Iterator<String> it = a.iterator(); while(it.hasNext()){
String temp = it.next(); if(刪除元素的條件){
     it.remove();
    }
}
複製程式碼

此時remove是iterator的remove,我們看一下它的原始碼:

public void remove() {
          if (lastRet < 0)
              throw new IllegalStateException();
          checkForComodification();

          try {
              ArrayList.this.remove(lastRet);
              cursor = lastRet;   //index of last element returned;-1 if no such
              lastRet = -1;
             expectedModCount = modCount;
         } catch (IndexOutOfBoundsException ex) {
             throw new ConcurrentModificationException();
         }
     }
複製程式碼

注意第10行,第8行,所以此時程式不會有之前的問題。 但是手冊上推薦的方法,在多執行緒環境還是有可能出現問題,一個執行緒執行上面的程式碼,一個執行緒遍歷迭代器中的元素,同樣會丟擲CocurrentModificationException。 如果要併發操作,需要對iterator物件加鎖。


平時遍歷list,然後刪除某個元素的時候,如果僅僅刪除第一個且刪除之後呼叫break //代表著此時不會再去執行iterator.next方法 也就不會觸發萬惡之源 而如果要刪除所有的某元素,則會報錯,謹記! Ps再來看一個佐證

public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        for(int i : list){
            System.out.println(i);
            if(i == 2){
                list.remove((Object)2);
            }
        }

    }
複製程式碼

此時只會輸出 1 2 當把remove物件改為3時候,再次報錯。

相關文章