测试代码:
List<String> list=new ArrayList<String>();
list.add("1");
list.add("2");
/*
方式1:
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals("2")){
list.remove(list.get(i));
}
方式2:会移除所有符合条件的,底层与方式3类似
list.removeIf(s -> s.equals("2"));
方式3:
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
if ("2".equals(iterator.next())) {
iterator.remove();
}
}
*/
for (String s : list) {
//在此处打断点
if (s.equals("2")){
list.remove(s);
}
}
源码探究:
经过断点查看,发现for (String s : list) {这里每次都会调用next方法:
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];
}
final void checkForComodification() {
//如果移除的是最后一元素,那么list队列,
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
发现如果移除,就会导致异常:
异常的原因就是预期的计数和实际计数不一致导致的。
expectedModCount 预期的模块计数
modCount 模块计数
进一步探查:
remove方法在arraylist的源码实现打断点:
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;
}
remove修改计数的地方就在fastRemove:
private void fastRemove(int index) {
//首先给实际计数+1
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将移除后腾出的空位设置为null,助于垃圾回收
elementData[--size] = null; // clear to let GC do its work
}
在这个方法单方面修改了modCount,导致后面modCount与expectedModCount 不一致。
同理,通过是在foreach方法的lambda表达式中,调用remove也是一样:
list.forEach(s->{
if (s.equals("1")){
list.remove(s);
}
});
forEach底层调用:
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
//也有计数校验
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
总结:
因此只要调用了remove方法,导致list长度变化,就会导致modCount != expectedModCount。
那么
foreach自带计数校验,加强for是每次都直接调用next方法(也会调用计数校验),因此导致触发异常。
fori循环,只是每次判断i < list.size(),直接根据实际list的索引拿数据,不会触发异常,但会导致数据移除不全的情况。比如list有三个数字{1,1,3},移除前,list[0]是1,list[1]是1,list[2]是3,循环判断时,通过remove移除等于1的数,那么第一次正常移除后,而list就变成了{1,3},而此时i++后i就由0变为1,可是此时被移除的元素后面的每个元素的索引都减了1,导致list[1]是3。这样第二个就不会被移除。
如果需要移除可以使用:
list.removeIf(s -> s.equals("2"));
或者迭代器,完全不涉及到计数校验的代码,也没有其他的缺陷,先取出list的迭代器,后面list移除数据,就与迭代器没关系了,最后list将迭代器中符合条件的元素进行直接移除。
简单总结:
只要移除后,调用计数校验的代码,都会出现异常。
探讨Java ArrayList在遍历过程中删除元素引发的ConcurrentModificationException异常原因,对比不同删除方式,包括for循环、foreach、迭代器及removeIf方法的优劣。
4006

被折叠的 条评论
为什么被折叠?



