不要在foreach迴圈里進行元素的remove/add操作。 remove元素請使用Iterator方式,如果併發操作,需要對Iterator對象加鎖。 正例 List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); ...
不要在foreach迴圈里進行元素的remove/add操作。 remove元素請使用Iterator方式,如果併發操作,需要對Iterator對象加鎖。
- 正例
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (刪除元素的條件) {
iterator.remove();
}
}
- 反例
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
說明: 以上代碼的執行結果肯定會出乎大家的意料,那麼試一下把"1"換成"2",會是同樣的結果嗎?
上面這一段摘自《Java開發手冊-嵩山版》編程規約-集合處理-第14條,最後一行的說明並沒有給出答案。因此自己琢磨這驗證一下。
代碼驗證
- 嘗試移除元素"1"
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
System.out.println(list); //輸出[2]
- 嘗試移除元素"2"
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
if ("2".equals(item)) {
list.remove(item);
}
}
System.out.println(list);
這裡涉及到一個語法糖-增強for迴圈,從錯誤日誌中可以看出增強for迴圈遍歷list時涉及到ArrayList$Itr。
上述代碼經過編譯器處理後的class文件進行反編譯,得到如下的代碼片段
List<String> list = new ArrayList();
list.add("1");
list.add("2");
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
String item = (String)iterator.next();
if ("2".equals(item)) {
list.remove(item);
}
}
問題定位
通過日誌定位找到拋異常的代碼
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
從上述這段代碼可以得出當modCount!=expectedModCount導致拋出的異常。
摘錄ArrayList$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 boolean hasNext() {
return cursor != size;
}
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();
}
}
}
}
查看ArrayList$Itr可以發現expectedModCount在創建Itr時通過modCount進行賦值,除此之外只有在remove()方法中重新通過modCount進行賦值。
重新查看測試代碼後,摘錄ArrayList中的相關代碼
public Iterator<E> iterator() {
return new Itr();
}
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
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
}
通過上述這段代碼可以定位到問題了,在調用list.remove()->remove()->fastRemove()中修改了modCount,導致下一次調用next()方法時報異常
分析到這裡細心的朋友可能又有一個新的問題了,元素"2"明明已經是list中的最後一個節點了,為什麼還會再次調用到next()方法呢?
答案是list.remove()操作後list的size值會減1,而迭代器的hasNext()是通過cursor和size是否相等來判斷是否還有下一個元素的。
至此,移除元素"2"報錯的原因就分析完畢了。下麵開始分析移除元素"1"時,為什麼程式沒有報異常?
List<String> list = new ArrayList();
list.add("1");
list.add("2");
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
String item = (String)iterator.next();
if ("1".equals(item)) {
list.remove(item);
}
}
可以很容易的發現兩段代碼的區別在於移除的元素在list中所處的位置不一樣。經過對移除元素"2"的代碼分析可以得知,問題的癥結在於list.remove()方法修改modCount參數。所以我們還是從這個方法開始分析。
通過上述代碼可以發現fastRemove()的原理是通過數組拷貝的方式將後一個元素的值拷貝到當前元素所在的位置。拷貝結束後尾節點置null,size-1。
在下一次調用hasNext()時結果為false導致迭代結束。
通過列印元素可以發現,移除元素"1"以後,元素"2"並沒有被列印出來。
總結
- 此問題出現的原因就是調用了list的remove(Object)方法而不是採用迭代器itr.remove()方法進行元素移除。
- 遍歷集合時需要對集合進行增刪操作,統一採用迭代器的方式進行。
- 所有的元素操作都是通過迭代器進行的,因此要進行併發操作時對迭代器加鎖是比較合適的一種手段。