最近在看ArrayList源码的时候,做了一个ArrayList循环遍历删除的列子,遇到了两个坑,和大家分享一下
情况一:
public class TestDelete {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
list.add("B");
list.add("C");
list.add("C");
list.add("C");
list.add("D");
list.add("D");
list.add("D");
list.add("D");
System.out.println("删除前:" + list.toString());
for(int i=0; i < list.size(); i++){
if(list.get(i).equals("B")){
list.remove(list.get(i));
}
}
System.out.println("删除后:" + list.toString());
}
}
结果:
删除前:[A, B, B, C, C, C, D, D, D, D]
删除后:[A, B, C, C, C, D, D, D, D]
情况二:
for(String str: list){ //等同于 for(Iterator i = list.iterator; i.hasNext(); )
if(str.equals("C")){
list.remove(str);
}
}
结果:
删除前:[A, B, B, C, C, C, D, D, D, D]
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.wxx.bigdata.hadoop.hdfs.arrayListTest.TestDelete.main(TestDelete.java:24)
查看JDK的ArrayList源码,先看下ArrayList中的remove方法(注意ArrayList中的remove有两个同名方法,只是入参不同,这里看的是入参为Object的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;
}
一般情况下,该方法都会走else分支,最终调用fastRemove方法,代码如下:
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
}
可以看到会执行System.arraycopy方法,导致删除元素时涉及到数组元素的移动复制。针对情况一,在遍历第一个字符串"B"时因为符合删除条件,所以将该元素从数组中删除,并且将后一个元素移动(也就是第二个字符串b)至当前位置,导致下一次循环遍历时后一个字符串"B"并没有遍历到,所以无法删除。针对这种情况可以倒序删除的方式来避免:
for(int j = list.size()- 1; j>=0; j--){
if(list.get(j).equals("B")){
list.remove(list.get(j));
}
}
结果如下:
删除前:[A, B, B, C, C, C, D, D, D, D]
删除后:[A, C, C, C, D, D, D, D]
因为数组倒序遍历时即使发生元素删除也不影响后序元素遍历。
接着解释一下情况二的错误原因。其产生的原因却是foreach写法是对实际的Iterable、hasNext、next方法的简写,问题同样处在上文的fastRemove方法中,可以看到第一行把modCount变量的值加一,但在ArrayList返回的迭代器(该代码在其父类AbstractList中):
public Iterator<E> iterator() {
return new 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();
}
}
通过看上述第一段代码,返回了一个Iterator的实现类Itr,第二段代码中,Itr实现了remove(), next()等方法,这两个方法中,有一个checkForComodification()方法,假如在remove第一个元素的时候,程序停掉,那么不会报错,直到再次调用next方法时,会判断"modCount != expectedModCount",不一致就会抛出"ConcurrentModificationException"。
这里会做迭代器内部修改次数检查,因为上面的remove(Object)方法修改了modCount的值,所以才会报出并发修改异常。要避免这种情况的出现则在使用迭代器迭代时(显示或for-each的隐式)不要使用ArrayList的remove,改为用Iterator的remove即可。
Iterator<String> it = list.iterator();
while (it.hasNext()){
String str = it.next();
if(str.equals("B")){
it.remove();
}
}
结果为:
删除前:[A, B, B, C, C, C, D, D, D, D]
删除后:[A, C, C, C, D, D, D, D]
还有一种解决方案,是先通过遍历拿到要删除的临时集合,然后调用removeAll方法删除临时集合
List<String> tmp = new ArrayList<String>();
for(String s : list){
if(s.equals("B")){
tmp.add(s);
}
}
list.removeAll(tmp);