Java中使用foreach遍历集合删除元素所引发的灾难

阿里巴巴Java开发手册中有这样一条规定:

【强制】不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。

那么,如果在foreach循环里进行元素的remove/add操作,会发生什么呢?我们来试试看!

运行下列代码:

import java.util.ArrayList;
import java.util.List;

public class ListTest {
    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.add("Allen");
        list.add("Bob");
        list.add("Edward");

        for (String s : list) {
            if (s.equals("Edward")) {
                list.remove(s);
            }
        }
    }
}

结果如下:

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 ListTest.main(ListTest.java:11)

Process finished with exit code 1

结果确实不可思议,众所周知,foreach只是Java中的一个语法糖,我们来看看把class文件反编译后真正运行的代码是什么:

public class ListTest {
    public ListTest() {
    }

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("Allen");
        list.add("Bob");
        list.add("Edward");
        Iterator var2 = list.iterator();

        while(var2.hasNext()) {
            String s = (String)var2.next();
            if (s.equals("Edward")) {
                list.remove(s);
            }
        }

        System.out.println(list);
    }
}

可以看到,针对Collection的foreach会被转化为while+Iterator来实现,为了搞清楚异常发生的原因,我们深入到ArrayList的Iterator中去一探究竟。

    public Iterator iterator() {
        return new Itr();
    }

ArrayList中的iterator方法返回的是其内部类Itr的实例,我们看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];
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
        ...
    }

不难发现,在next方法的开始位置就要执行一个checkForComodification检查,而正是这个检查抛出了ConcurrentModificationException异常。checkForComodification检查的是modCount和expectedModCount是否相等,顾名思义,modCount代表对该集合的修改次数,即,如果在Iterator遍历过程中由其它未知因素对集合进行了修改,那么将会发生并发修改异常。

那么,是哪里对modCount进行了修改呢?我们来看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++;
        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
    }

不难发现,在fastRemove中对modCount进行了自增操作,从而导致在Iteraor自检时modCount和expectedModCount不等产生了ConcurrentModificationException异常。

那如果我们想对集合进行条件删除,该怎么做呢?推荐两种方法。

方法1:使用Iterator的remove方法

我们之前是使用的List的remove方法导致产生了异常,而使用Iterator的remove方法就不会产生问题,因为Iterator的remove方法统一了modCount和expectedModCount的修改。

    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 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();
            }
        }
}

方法2:使用集合类自带的removeIf方法

只需传入一个谓词,就可完成遍历+条件删除,很方便。

import java.util.ArrayList;
import java.util.List;

public class ListTest {
    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.add("Allen");
        list.add("Bob");
        list.add("Edward");

        System.out.println(list);
        list.removeIf(s -> s.equals("Edward"));
        System.out.println(list);
    }
}


每日学习笔记,写于2020-05-31 星期日

你可能感兴趣的:(Java中使用foreach遍历集合删除元素所引发的灾难)