- 首先,出现该异常的代码:
List<String> strs = new ArrayList<>();
strs.add("1");
strs.add("2");
strs.add("3");
strs.add("4");
strs.add("5");
for (String item : strs) {
if (item.equals("3")) {
strs.remove(item);
}
}
执行上面这段代码是会抛出ConcurrentModificationExcepiton异常的,至于为什么我们待会儿在源码的分析当中再给出答案。同样我们再来看下面这段代码:
List<String> strs = new ArrayList<>();
strs.add("1");
strs.add("2");
strs.add("3");
strs.add("4");
strs.add("5");
for (int i = 0; i < strs.size(); i++) {
String str = strs.get(i);
if (str.equals("3")) {
strs.remove(str);
}
}
而再执行这段代码的时候是不会抛出ConcurrentModificationException的,同样是for循环,差别在哪里?
我们知道,第一种方法里面用到的是foreach循环,而foreach循环的具体实现是Iterator,每次循环的时候都会调用Iterator中的hasNext()方法,该方法返回为true则调用next()方法获得该元素,这就是这两段代码产生不同结果的原因。那么为什么使用了Iterator就抛出该异常呢?这时候如果你想起另外一种实现遍历List的方式,可能就会对我所说的使用了Iterator就会抛出异常产生疑问。直接上代码
List<String> strs = new ArrayList<>();
strs.add("1");
strs.add("2");
strs.add("3");
strs.add("4");
strs.add("5");
Iterator<String> iterator = strs.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
if (next.equals("3")) {
iterator.remove();
}
}
当我们再次运行上面这段代码的时候,发现是能正常删除集合中等于“3”的这个元素的,那么问题来了,刚才也说到了,使用foreach来循环List并且删除指定的某个元素时会抛出异常,并且其内部实现也是Iterator,那么它与第三种实现方式的差别到底在哪儿?
当我们分析到这儿的时候,我们可以得出这样一个结论,当我们使用到Iterator的时候ConcurrentModificationException异常才有可能会发生,那么我们从源码找答案的时候就直接从集合的迭代器Iterator中来查找。以ArrayList为例:
ArrayList.iterator()方法得到的实例为class Itr,Itr是ArrayList的内部类。我们只关注Itr中的hasNext()、next()、remove()、checkForComodification()这几个方法。
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();
}
}
在初始化Itr的时候,会将modCount的值赋给expectedModCount,其中modCount为The number of times this list has been structurally modified,每当调用ArrayList的add()、remove()等方法的时候都会执行modCount++。
回到我们用foreach循环List的代码中,当调用strs.remove(item)后,这时modCount会加1,而expectedModCount的值没变,接着会调用hasNext()方法检查是否已经执行到最后一个元素,在上述代码中显然还不是最后一个元素,所以接着执行next()方法,注意,进入next()方法后会先执行checkForComodification()方法,而该方法只做了判断modCount与expectedModCount是否相等这一件事,从前面的分析可以看出来这时候两者的值是不相等的,所以抛出ConcurrentModificationException。
那使用Iterator来迭代ArrayList的时候调用Iterator.remove()方法的时候为什么就没有抛出异常呢?答案就在于调用Iterator.remove()方法除了会调用ArrayList中的remove()方法外,还会将改变后的modCount值再次赋给expectedModCount,使得两者再次相等,从而在下次调用checkForComodification()方法检查时不会抛出异常。
还有一个小的知识点,就是在第一段代码的foreach方法中,如果我们删除的是倒数第二个元素,是不会抛出异常的。那么这又是为什么呢?前面已经提过,foreach方法是Iterator来实现的,这时我们再次查看Itr中的代码。next()方法中获取下一个元素其实是通过不断的向后移动游标cursor,从而在数组中取得对应的元素(ArrayList的底层实现仍然是数组)。游标cursor的移动过程点击查看
在这里我们主要关心当取到倒数第二个元素,即图中角标为5的元素的时候,这时cursor的值为6。取到值后,接着会调用ArrayList.remove()方法,如下:
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index); //实际删除元素在该方法中
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++; // 对ArrayList modify的次数,每当调用add()、remove()方法都会加1
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved); // 该方法将数组中最后一个元素的值赋给倒数第二个元素,没有改变数组的大小,数组大小为7。
elementData[--size] = null; // 关键点在这儿,这时会将数组中的最后一个元素的值设置为null,并且将size减1,size表示的是集合的大小,这时候size的值为6.
}
当成功删除倒数第二个元素的时候,Iterator会先调用hasNext方法查看是否还有未取完的元素,正常情况选,应该继续读取最后一个元素,但是从上面的分析中知道,这时候的size值为6,cursor的值也为6,而hasNext方法返回tre的条件是两者不相等,所以程序走到这里就退出了foreach循环。
分析到这里,对于为什么删除倒数第二个元素不会抛出ConcurrentModificationException异常的原因已经非常明显,因为这时没有遍历到集合中的每一个元素,所以这种写法也是不正确的。