在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异常完美解决。