本文主要分析在循环遍历的情况下使用ArrayList删除元素可能会导致的问题,并给出解决方案。如果有更好的解决方案请务必留言。万分感谢!
List list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
for (int i = 0;i
错误:第二个“2”不会被删除掉。
分析如下:arraylist删除方法源码如下:
//remove方法,最终会调用fastRomove()
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++;
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
}
这段代码主要逻辑是:如果arraylist删除了一个元素,则将此元素后的所有元素往前移一位,并将集合长度-1。
这里简单介绍一下System.arrayCopy()方法:如果两个数组相同,则将原数组的(index+1)到(index+1)+numMoved位置的元素平移到index到index+numMoved位置。
因为元素移动,所以在遍历到第一个“2”(下标为1)并删除掉后,第二个紧随的“2”会移动到下标为1的位置。但下次遍历时,会从下标2继续遍历。所以会有漏网之鱼。
解决方案:可以采用倒序的遍历方式来避免遗漏的情况发生:
for (int i = list.size()-1; i >=0 ; i--) {
list.remove("2");
}
这段代码执行后会抛出一个异常。
for (String str:list) {
if ("2".equals(str)){
list.remove(str);
}
}
异常如下:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at com.lifeisftc.collection.list.ArrayListRemoveError.main(ArrayListRemoveError.java:32)
下边逐步分析:
1.众所周知加强for循环是使用 Iterator 中的方法实现的。首先寻找ArrayList中使用的 Iterator。并找到hasNext()和next()方法
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator {
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];
}
通过debug调试,发现问题出现在next()方法中的第一行:checkForComodification();翻看此方法代码:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
发现在modCount和expectedModCount不相等时,会抛出此异常。这两个属性的含义是:
modCount:modCount到底是干什么的,所以对集合进行的任何增、删、改操作都会使该值+1.
expectedModCount:在Itr类初始化时将modCount值复制过来。也就是在加强for循环开始时,expectedModCount的值已经确定了。那么在之后的循环过程中,如果进行了增删改操作,modeCount++,但expectedModCount不变,在checkForComodification()方法中,就会抛出此异常导致遍历结束。
另外的:如果删除的是集合中倒数第二个元素时,不会抛出此异常,因为在调用next方法时,首先会调用hasNext()方法,此时,由于删除元素,集合的长度减少1,hasNext()方法false,遍历停止。
解决方法:不要在加强循环中使用集合的方法删除元素。可以使用Iterator中的remove方法删除元素,此时不会抛出异常。但在多线程环境下,要考虑这样做会不会造成问题和影响。
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();
}
}