《阿里巴巴Java開發手冊》第一章裡的第五節的第七點是這麼說的:
【強制】不要在 foreach 迴圈裡進行元素的 remove/add 操作。remove 元素請使用 Iterator 方式,如果併發操作,需要對 Iterator 物件加鎖。
裡面舉了這樣一個反例:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
複製程式碼
其實Java的forEach寫法內部就是迭代器,大家可以把上面的程式碼理解為以下程式碼:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("1".equals(item)) {
list.remove(item);
}
}
複製程式碼
有了這一層理解後,那我們以ArrayList為例,看看其內部的iterator
方法:
public Iterator<E> iterator() {
return listIterator();
}
public ListIterator<E> listIterator(final int index) {
checkForComodification();
rangeCheckForAdd(index);
final int offset = this.offset;
return new ListIterator<E>() {
hasNext()...
next()...
...
}
}
複製程式碼
由於listIterator()
方法內的內部類ListIterator
的程式碼太多,我就不一一貼出來了,因為我們重點只看兩個方法:hasNext()
和next()
,接下來我會通過斷點除錯讓大家明白為什ConcurrentModificationException
是偶爾出現:
斷點除錯
設定斷點
我在這三處地方都打了斷點,這樣我們就能大概清楚整個流程:
執行除錯
P1
好的,我們看到已經定位到第一個斷點位置了,從idea提供的資訊我們也可以看出list的大小為2:
接著往下走,又來到了第二個斷點的位置,在上面我已經說了forEach
的語法的原理了,所以這樣會走到haxNext()
函式這裡,這裡的cursor
是指當前迭代器的指標,而size
是當前集合的大小:
繼續走,我們會來到第三個斷點:
圈紅1
注意我圈紅的第一處地方,我們進入checkForComodification裡:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
複製程式碼
可以看到,這就是我們報錯的關鍵點,這裡的modCount
變數是指集合被操作的次數,比如像add()
、remove()
這些方法都會讓modCount + 1
,而expectedModCount
是指集合的一個預期操作次數,在部分操作裡會被重置為modCount
,比如add()
方法裡。
因為我們上面新增了兩個元素,所以modCount
和modCount
都是2。
圈紅2
接著我們看第二處圈紅的地方,我們可以發現,每一次next()
的時候指標都會移動,這很好理解。
P2
斷點繼續,因為第一個元素就是1,所以這裡匹配上了:
我們進入到remove()
方法裡面,因為我們是按照物件刪除的,所以會進入第二個分支:
接著我們再進入fastRemove()
方法,可以看到modCount + 1
了:
P3
繼續往下走,我們又回到最開始的地方,但仔細點你會發現list的大小從2變成1了:
然後我們又來到了hasNext()
這裡了,因為cursor
和size
都是1,所以迴圈就終止了:
吃鯨
這裡你是不是懵逼了,咦?說好的報錯呢?怎麼沒報錯了?
咳咳,其實是因為有時候會出現像上面這種巧合的情況,就是在hasNext()
方法校驗的時候,cursor
剛好不等於size
,然後就退出了,而剛好集合又遍歷完了,but,這個情況是很少出現的,一般都會丟擲ConcurrentModificationException
異常,所以大家不要有僥倖的心理。
還原報錯
下面我們還是以上面的例子,只是這次我把刪除的物件從1改為2:
執行除錯後跟上面的P1和P2是一樣的,所以這裡我就不重複了,唯一不同的地方在P3。這裡我們已經來到第二次迴圈,校驗元素後會刪除元素2:
在第三次迴圈,(這裡是指第三次進行hasNext()
)的時候,我們可以看到list的大小是1了:
ok,我們繼續往下,這裡大家要特別注意,可以看到cursor
此時是2,而size
卻是1,所以迴圈還可以繼續:
前面我們說過next()
方法裡的checkForComodification()
是檢查操作次數的,所以這裡就不復述了:
我們進入到checkForComodification()
裡,可以看到modCount
是3(因為remove()
操作**modCount**
自增了),而expectedModCount
是2,所以就報錯了: