一、前言
Java中,集合类ArrayList不管是在开发工作中,还是在面试中,都应该是个比较高频出现的知识点。在使用过程中,可能会遇到迭代删除的需求场景,此时如果代码书写不当,极有可能会抛出 java.util.ConcurrentModificationException 异常信息。下面对这个异常做点分析,为什么会出现异常,怎样去正确的迭代删除。
二、异常原因分析
测试代码如下:
package com.cfang.prebo.oTest; import java.util.Iterator; import java.util.List; import com.alibaba.fastjson.JSON; import com.google.common.collect.Lists; public class TestListException { public static void main(String[] args) { List<Integer> list = Lists.newArrayList(); list.add(1); Iterator<Integer> iterator = list.iterator(); while(iterator.hasNext()) { Integer val = iterator.next(); if(val == 1) { list.remove(val); // iterator.remove(); } } System.out.println("result:" + JSON.toJSONString(list)); } }
运行结果:
从异常栈信息中可以看出,最终抛出此异常的方法,是 checkForComodification 方法。下面进行追根逐源的看看,为什么方法会抛出异常。
首先整体贴出迭代器 Iterator 对 List 进行迭代的关键性代码片段:
public Iterator<E> iterator() { return new Itr(); } /** * An optimized version of AbstractList.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(); } }
对List的迭代iterator会new出个Itr对象的引用,Itr是个成员内部类实现。其中几个关键性的属性:
cursor - 游标索引,表示下一个可访问的元素的索引
lastRet - 还是索引,是上一个元素的索引。默认值-1
expectedModCount - 对集合的修改期望值,初始值等于modCount
modCount的定义在AbstractList中,初始值为0,如下定义:
protected transient int modCount = 0;
该值会在List的方法add以及remove中,进行加1操作,如下代码片段:
public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); 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 return oldValue; }
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { if (elementData == EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
也就是说,在进行add和remove的时候,都会将modCount值修改。
好了,铺垫到这里,就可以来结合测试main方法来进行一步步的解释说明了:
1、初始化ArrayList,调用list.add方法,此时,modCount=1,list.size = 1
2、初始化itreator迭代循环。此时,expectedModCount = modCount = 1,cursor默认值0,lastRet默认值-1
3、itreator.hasNext方法判断,cursor != size成立,有元素可访问,进入循环
4、调用itreator.next方法,校验后获取值。此时expectedModCount == modCount成立,校验通过。获取值并设置相关属性 lastRet = 0,cursor = 1
5、调用list.remove方法,modCount加1。此时,modCount=2,list.size = 0
6、itreator.hasNext方法判断,cursor != size成立,进入循环
7、调用itreator.next方法,校验方法checkForComodification,此时,expectedModCount != modCount成立,抛出ConcurrentModificationException异常
写到这里,基本上为啥会出现异常,应该是已经非常明了清晰了。总结起来就是:如果是使用list.remove的话,会导致expectedModCount != modCount条件成立,也即两个的值会不等。当然了,使用for-each迭代也是一样的,毕竟for-each底层如果是对集合遍历的话,也还是利用itreator去做的。
说完原因呢,下面简单说说解决办法:
单线程情况下:可以使用迭代器itreator提供的remove,从源码中可以看出,在方法中会对cursor、lastRet重设值,将expectedModCount重新设值为modCount。
多线程情况下:1、迭代删除使用锁 - synchronized或者lock
2、创建安全的容器 - Collections.synchronizedList方法、CopyOnWriteArrayList