最近在看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);