天天看點

深入剖析Java foreach實作原理,避免ConcurrentModificationException

文章目錄

    • 認識foreach
    • 了解使用限制,防止錯誤使用
    • 錯誤使用
    • 正确使用
    • 總結

并發修改異常是指:

ConcurrentModificationException

認識foreach

foreach循環是JDK1.5開始引入的,這種方式周遊集合或數組,代碼更加簡潔。

foreach循環本質上來說可以周遊任何實作了Iterable接口的對象。

foreach本質上不過是編譯器提供的“文法糖”包裝。編譯器在遇到

for(Type item : arrayOrList) { }

代碼時,會進行代碼的轉譯。

如果是數組,會轉成數組的周遊方式:

for(int i=0;i<array.length;i++) {
    Type item = array[i];
    /* body-of-loop */
}
           

如果是周遊集合類型,則要求被周遊的集合類型實作

java.lang.Iterable

接口,在iterator()方法中傳回一個Iterator疊代器。

//Iterable.java
public interface Iterable<T> {
    Iterator<T> iterator();
}
//Iterator.java
public interface Iterator<E> {
    boolean hasNext();
    E next();
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
}
           

相應的foreach代碼會被編譯器轉換成Iterator的疊代方式:

for(Iterator<Type> iter = list.iterator(); iter.hasNext(); ) {
    Type item = iter.next();
    /* body-of-loop */
}
           

了解使用限制,防止錯誤使用

foreach并不是萬能的。在某些場景下不能使用。

  1. foreach在周遊過程中不能修改集合中元素的值。不過,如果周遊的是數組,則不受此限制。
  2. foreach在周遊過程中不能往集合中增加或删除元素,否則ConcurrentModificationException異常。即使在個别特殊情況下沒有抛出這個異常,那也是因為巧合(下文會有說明)。
  3. 周遊過程中,集合或數組中同時隻有一個元素可見,即隻有“目前周遊到的元素”可見,而前一個或後一個元素是不可見的。
  4. 隻能從前往後正向周遊,不能反向周遊。

錯誤使用

關于第2點,嘗試在foreach周遊過程中,添加或删除元素:

ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
final String toRemove = "2";
final String toAdd = "1000";
for (String item : list) {
    //item = "100"; //這句執行無效,僅僅改變item的指向,不會改變list中的元素
    if (toRemove.equals(item)) {
        list.remove(item); //僅當toRemove為"3"時,沒有報異常。這是删除倒數第二個元素情況下的“巧合”。
        //list.add(toAdd); // 報ConcurrentModificationException
    }
}
           

原因分析:

  1. ArrayList内部有一個成員變量

    modCount

    ,記錄list内部元素改變的次數。
  2. 通過

    iter=list.iterator()

    傳回一個新的疊代器對象的時候,iter内部會用

    expectedModCount

    成員變量記錄下當時的

    modCount

    的值。在整個循環的周遊過程中,不管是

    iter.next()

    還是

    iter.remove()

    方法都會檢查原ArrayList的

    modCount

    值是否與iter内部記錄的

    expectedModCount

    值一緻,一旦不一緻就會抛

    ConcurrentModificationException

    。是以,異常是在增減集合元素後,下一輪循環的

    iter.next()

    方法中抛出的。
  3. 相關源代碼
public boolean hasNext() {
    return cursor != size;
}
public E next() {
	// 此處抛出異常
    checkForComodification();
    // 省略其他代碼...
}
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
           
  1. 當toRemove為"3"時,沒有報異常,這是為什麼呢?

    因為,這種情況下,一旦remove之後,原ArrayList的size會減少1,下一輪通過iter.hasNext()(hasNext隻是傳回疊代器内部的疊代位置cursor是否已達到被疊代容器的size,本身不會抛異常)判斷是否還有元素時,發現沒有了,直接傳回false,進而不會調用到

    iter.next()

    方法。當然也就不會有從這個方法中抛出的異常啦。

正确使用

那麼問題來了,如果要在周遊集合的過程中需要删除或添加元素該怎麼辦?

  1. 普通for循環
    for(int i=0;i<list.size();i++) {
        String item = list.get(i);
        if ("3".equals(item)) {
            list.remove(i);//為了效率,這裡最好不要用list.remove(item)
        }
    }
               
  2. Iterator疊代器方式周遊,通過疊代器的remove方法進行删除。
    Iterator<String> iter = list.iterator();
    while(iter.hasNext()) {
        String item = iter.next();
        if ("4".equals(item)) {
            iter.remove();
        }
    }
               

總結

在 subList 場景中,高度注意對父集合元素的增加或删除,均會導緻子清單的周遊、

增加、删除産生 ConcurrentModificationException 異常。——阿裡巴巴Java開發規範(嵩山版)

好了,以上就是全部内容。