在java語言中,ArrayList是一個很常用的類,在編程中經常要對ArrayList進行刪除操作,在使用remove方法對ArrayList進行刪除操作時,報java.util.ConcurrentModificationException異常,下面探討一下該異常的原因以及解決辦法。
1 import java.util.ArrayList; 2 import java.util.List; 3 4 public class Test { 5 6 public static void main(String[] args) { 7 // TODO Auto-generated method stub 8 List<Integer> listA=new ArrayList<>(); 9 listA.add(1); 10 listA.add(2); 11 listA.add(3); 12 listA.add(4); 13 listA.add(5); 14 listA.add(6); 15 16 for(Integer a:listA){ 17 if (a==3) { 18 listA.remove(3); 19 } 20 } 21 } 22 }
上述代碼在刪除value=3的元素時,報java.util.ConcurrentModificationException異常,如下圖
1 Exception in thread "main" java.util.ConcurrentModificationException 2 at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) 3 at java.util.ArrayList$Itr.next(ArrayList.java:851) 4 at com.zhang.Test.main(Test.java:19)
那么是不是刪除每一個元素都會報上面的異常呢?經過測試發現,刪除value=5的元素(倒數第二個元素)時,就不會報上面異常,除此之外均會報如上異常。(此處不再一一測試,有興趣可自己編碼測試)
我們通過上述測試發現了規律,既除了刪除倒數第二個錯誤不會異常,刪除其他元素均會異常。既然掌握了規律,那么就要從源碼層面揭露該異常的原因。首先發現Java的for循環,就是將List對象遍歷托管給Iterator,你如果要對list進行增刪操作,都必須經過Iterator,否則Iterator遍歷時會亂,所以直接對list進行刪除時,Iterator會拋出ConcurrentModificationException異常。
其實,每次foreach迭代的時候都有兩部操作:
第一步:iterator.hasNext() //判斷是否有下個元素
public boolean hasNext() { return cursor != size; }
第二步:item = iterator.next() //下個元素是什么,並賦值給上面例子中的item變量
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]; }
通過debug調試,我們發現,checkForComodification時返回了異常,異常原因為 modCount != expectedModCount。
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
進一步閱讀源碼,發現:
1.modCount 時List從new開始,被修改的次數。當List調用Remove等方法時,modCount++
2.expectedModCount是指Iterator現在期望這個list被修改的次數是多少次。是在Iterator初始化的時候將modCount 的值賦給了expectedModCount
那么就解釋了為什么會報上述異常:
1.modCount 會隨着調用List.remove方法而自動增減,而expectedModCount則不會變化,就導致modCount != expectedModCount。
2.在刪除倒數第二個元素后,cursor=size-1,此時size=size-1,導致hasNext方法認為遍歷結束。
public class Test { public static void main(String[] args) { // TODO Auto-generated method stub List<Integer> listA=new ArrayList<>(); listA.add(1); listA.add(2); listA.add(3); listA.add(4); listA.add(5); listA.add(6); for(Integer a:listA){ System.out.println(a); if (a==5) { listA.remove(5); } } } }
上述代碼加了打印后,輸出1,2,3,4,5;進一步證明最后一個元素6並沒有被遍歷到。
解決方法1:
從API中可以看到List等Collection的實現並沒有同步化,如果在多 線程應用程序中出現同時訪問,而且出現修改操作的時候都要求外部操作同步化;調用Iterator操作獲得的Iterator對象在多線程修改Set的時 候也自動失效,並拋出java.util.ConcurrentModificationException。這種實現機制是fail-fast,對外部 的修改並不能提供任何保證。
網上查找的關於Iterator的工作機制。Iterator是工作在一個獨立的線程中,並且擁有一個 mutex鎖,就是說Iterator在工作的時候,是不允許被迭代的對象被改變的。Iterator被創建的時候,建立了一個內存索引表(單鏈表),這 個索引表指向原來的對象,當原來的對象數量改變的時候,這個索引表的內容沒有同步改變,所以當索引指針往下移動的時候,便找不到要迭代的對象,於是產生錯 誤。List、Set等是動態的,可變對象數量的數據結構,但是Iterator則是單向不可變,只能順序讀取,不能逆序操作的數據結構,當 Iterator指向的原始數據發生變化時,Iterator自己就迷失了方向。
如何才能滿足需求呢,需要再定義一個List,用來保存需要刪除的對象:
List delList = new ArrayList();
最后只需要調用集合的removeAll(Collection con)方法就可以了。
解決方法2:
在找到原因后,則進一步進行解決
經過查閱源碼可以發現,iterator也有一個remove方法如下,其中有一個重要的操作為expectedModCount = modCount;這樣就保證了兩者的相等。
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(); } }
修改后的代碼如下:
public class Test { public static void main(String[] args) { // TODO Auto-generated method stub List<Integer> listA=new ArrayList<>(); listA.add(1); listA.add(2); listA.add(3); listA.add(4); listA.add(5); listA.add(6); Iterator<Integer> it_b=listA.iterator(); while(it_b.hasNext()){ Integer a=it_b.next(); if (a==4) { it_b.remove(); } } for(Integer b:listA){ System.out.println(b); } } }
java.util.ConcurrentModificationException異常完美解決。