List<String> list = new ArrayList<>(); list.add("str1"); list.add("str2"); list.add("str3"); for (String s : list) { if ("str1".equals(s)) { list.remove(s); } }
這段代碼看起來好像沒有什么問題,但是如果我們運行,就會拋出ConcurrentModificationException異常。
其實這不是特例,每當我們使用迭代器遍歷元素時,如果修改了元素內容(添加、刪除元素),就會拋出異常,由於 foreach 同樣使用的是迭代器,所以也有同樣的情況,大家可以自行試一下。我們來通過源碼探究一下這個現象的根本原因。
ArrayList 源碼閱讀
下面是 ArrayList 的部分源碼,可以明顯的看到共有兩個remove()
方法,一個屬於 ArrayList 本身,還有一個屬於其內部類 Itr。
public class ArrayList<E> { void remove() { modCount++; // 繼承自AbstractList的屬性,保存對其中元素的修改次數,每次增加或刪除時加1 // 具體刪除操作代碼 //... } public Iterator<E> iterator() { return new Itr(); } private class Itr implements Iterator<E> { int cursor; // index of next element to return // 在創建迭代器時將當前ArrayList的修改次數賦值給 expectedModCount 保存 int expectedModCount = modCount; public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { // 檢查當前所在的 ArrayList 的 modCount 是否與創建 Itr 時的值一致, // 也就是判斷獲取了Itr迭代器后 ArrayList 中的元素是否被 Itr 外部的方法改變過。 checkForComodification(); // 具體的獲取下一個元素的代碼 // ... } public void remove() { if (lastRet < 0) throw new IllegalStateException(); // 同 next 中的 checkForComodification 方法 checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; // Itr 內部的刪除元素操作,會更新 expectedModCount 值,而外部的則不會 expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } } }
ArrayList 類中有一個 modCount 屬性,這個屬性是繼承子AbstractList,其保存了我們對 ArrayList 進行的的操作次數,當我們添加或者刪除元素時,modeCount 都會進行對應次數的增加。
迭代器迭代時外部方法添加或刪除元素
在我們使用 ArrayLis 的 iterator()
方法獲取到迭代器進行遍歷時,會把 ArrayList 當前狀態下的 modCount 賦值給 Itr 類的 expectedModeCount 屬性。如果我們在迭代過程中,使用了 ArrayList 的 remove()
或add()
方法,這時 modCount 就會加 1 ,但是迭代器中的expectedModeCount 並沒有變化,當我們再使用迭代器的next()
方法時,它會調用checkForComodification()
方法,即
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
發現現在的 modCount 已經與 expectedModCount 不一致了,則會拋出ConcurrentModificationException
異常。
迭代器迭代時,使用迭代器的方法添加或刪除元素
但是如果我們使用迭代器提供的remove()
方法,由於其有一個操作:expectedModCount = modCount;
,會修改expectedModCount 的值,所以就不會存在上述問題。
總結
當我們使用迭代器迭代對象的時候,不要使用迭代器之外的方法修改元素,否則會報異常。如果我們要在迭代器迭代時進行修改,可以使用迭代器提供的刪除等方法。或者使用其他方法遍歷修改。
注意:
特殊情況下在迭代器過程中使用 ArrayList 的刪除方法不會報異常,就是 只刪除倒數第二個元素的時候,代碼如下:
List<String> list = new ArrayList<>(); list.add("str1"); list.add("str2"); list.add("str3"); for (String s : list) { if ("str2".equals(s)) { // 必須只能是倒數第二個元素,這樣才不會拋異常 list.remove(s); } }
其原因是迭代器的hasNext()
方法:
public boolean hasNext() { return cursor != size; }
在只刪除了倒數第二個元素的時候,cursor 會與 size 相等,這樣hasNext()
方法會返回 false ,結束迭代,也就不會進入 next()
方法中,進而執行checkForComodification()
檢查方法拋出異常。