先來看一段程式碼,摘自阿里巴巴的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);
複製程式碼
幾個需要注意的點:
- foreach遍歷集合,實際上內部使用的是iterator。
- 程式碼先判斷是否hasNext,然後再去呼叫next,這兩個函式是引起問題的關鍵。
- 這裡的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();
}
}
複製程式碼
幾個需要注意的點:
- 在iterator初始化的時候(也就是for迴圈開始處),expectedModCount = modCount,猜測是和當時list內部的元素數量有關係(已證實)。
- 當cursor != size的時候,hasNext返回true
- next()函式的第一行,checkForComodification()這個函式就是報錯的原因 這個函式就是萬惡之源
- 第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時候,再次報錯。