java.util.ConcurrentModificationException异常原因(附源码分析)
前言
今天看书看到关于for-each相比jdk1.5之前的for循环的优缺点,有一个地方说道
"for-each循环中不能使用list.remove()"
草草就带过去了,自己不太理解为什么,然后决定自己写一个简单的Demo进行测试一下。
public class WingzingDemo {
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
strList.add("AA");
strList.add("BB");
strList.add("CC");
strList.add("DD");
//strList.iterator();
for(String s:strList){
if(s.equals("BB")){
boolean remove = strList.remove(s);
System.out.println(remove);
}
}
System.out.println("size :" + strList.size());
}
}
在for-each中进行list.remove(),然后报错java.util.ConcurrentModificationException。
debug发现是执行了remove操作后的一次循环报错了,也就是第三次循环。
查阅源码分析
因为是ArrayList,循环其实也是进行迭代。
/**
* Returns an iterator over the elements in this list in proper sequence.
*
* <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
*
* @return an iterator over the elements in this list in proper sequence
*/
public Iterator<E> iterator() {
return new Itr();
}
发现进行迭代的时候是返回了一个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();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
继续查看其中的remove()方法
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
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;
}
发现了问题所在
System.arraycopy(Object src,int srcPos,Object dest,int destPos,int length);
//对于ArrayList的调用
//第一个参数为原数组,第二个参数为开始下标
//第三个参数为目标数组,第四个参数为开始下标,第五个参数为复制的长度
//也就是说把src的第srcPos位到srcPos+length位进行截取,复制到dest的destPos位。
在底层实现为数组的ArrayList的remove方法中,是把去掉了该元素后的"尾巴"复制到该数组,从该元素的下标。(具体可以看我上面代码里面写的备注。)这之后elementData把最后一位给设为空,后面会被GC给回收。
//这里涉及到集合的扩容机制,以及ArraList的不可变性。有兴趣可以点击查看ArrayList和LinkedList的区别、扩容机制以及底层实现。
再看回next()方法。
@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];
}
因为elementData的length被减了一个 所以进入了判断
if (i >= elementData.length)
throw new ConcurrentModificationException();
也就是数组长度和预计长度不一致报的错。
得证
书上说的这个情况确实存在,在其中调用了list.remove()方法会导致循环报错。以上是在Java8的环境下的源码,好像8之前的这个地方源码稍微有一点点出入,不过大概是一样的意思。
后言
所以在数组循环中,需要移除某个元素 还是得使用iterator.remove() 方法进行移除,所以也就不太适用for-each进行循环。
for(Iterator<String> iterator = strList.iterator();iterator.hasNext();){
String str = iterator.next();
if(str.equals("BB")){
iterator.remove();
}
}