Java中集合删除元素时候关于ConcurrentModificationException的迷惑点

来源:互联网 发布:易语言传奇辅助源码 编辑:程序博客网 时间:2024/06/06 13:23

本文在本人的cnblog中也有发表,并不是盗文。


下面的示例来至于阿里巴巴Java开发手册的集合处理部分的第7条:


运行如下代码,会发现正确运行。

复制代码
    public static void hasNotExcption() {        List<String> list1 = new ArrayList<String>();        list1.add("1");        list1.add("2");        for (String item : list1) {            System.out.println("item : "+item);            if ("1".equals(item)) {                list1.remove(item);            }        }    }
复制代码

但是运行如下代码,则异常:java.util.ConcurrentModificationException(和1中的代码区别是上面移除判断条件是1,下面的判断条件是2)

复制代码
    public static void hasExcption() {        List<String> list1 = new ArrayList<String>();        list1.add("1");        list1.add("2");        for (String item : list1) {            System.out.println("item : "+item);            if ("2".equals(item)) {                 list1.remove(item);            }        }    }
复制代码

 

再看如下代码示例,运行结果见注释:

复制代码
    /**     *没有异常     */    public static void hasNotExcption() {        List<String> list1 = new ArrayList<String>();        list1.add("1");        list1.add("2");        list1.add("3");        list1.add("4");        for (String item : list1) {            System.out.println("item : " + item);            // 移除1和3时候会抛ConcurrentModificationException异常            // 但是移除2的时候不会抛出异常            if ("3".equals(item)) {                list1.remove(item);            }        }    }    /**     * 有异常     */    public static void hasExcption1() {        List<String> list1 = new ArrayList<String>();        list1.add("1");        list1.add("2");        list1.add("3");        for (String item : list1) {// 增强的for循环底层实现使用的是迭代器            if ("1".equals(item)) {                // 移除并修改“modCount变量”,导致下次遍历时候异常                list1.remove(item);            }        }    }    /**     * 有异常     */    public static void hasExcption2() {        List<String> list1 = new ArrayList<String>();        list1.add("1");        list1.add("2");        list1.add("3");        for (String item : list1) {            if ("3".equals(item)) {                list1.remove(item);            }        }    }
复制代码

通过上面三个例子是不是发现了结论?只要移除的是倒数第二个元素的话,就不会发生异常!的确是,移除倒数第二个元素的话就不会异常,那究竟是为什么呢?

 

知识点1:关于增强for循环的实现

集合的增强for循环实现内部使用的是迭代器,可以通过eclipse的F5调试跟踪。

复制代码
        List<String> list1 = new ArrayList<String>();        list1.add("1");        list1.add("2");        list1.add("3");        for (String item : list1) {            if ("3".equals(item)) {                list1.remove(item);            }        }
复制代码

 

知识点2:集合如果判定的是并发修改错误?

细心的话可以通过异常发现异常的判断代码是:

if (modCount != expectedModCount)                throw new ConcurrentModificationException();

其中的modCount是记录集合的修改次数,而expectedModCount是记录的预期的修改次数,当集合的修改次数和预期修改次数不一致的时候则发生异常。

那么什么时候会修改modCount呢?当我们对集合进行删除或者添加元素时候此时记录集合的修改次数就会发生增加,但是期望的修改次数不会变化,当检查判断异常条件时候就会根据条件处理。

 

到此回归到上面问题,对于hasExcption1和hasExcption2是如何发生异常的?本例对hasExcption1进行详细解释:

复制代码
    /**     * 有异常     */    public static void hasExcption1() {        List<String> list1 = new ArrayList<String>();        list1.add("1");        list1.add("2");        list1.add("3");        // 增强的for循环底层实现使用的是迭代器。所以每一次都是调用一个hasNext,然后在调用next方法返回元素给item        for (String item : list1) {                        if ("1".equals(item)) {                // 移除并修改“modCount变量”,导致下次遍历时候异常                list1.remove(item);            }        }    }
复制代码

增强的for循环底层实现使用的是迭代器。所以每一次都是调用一个hasNext,然后在调用next方法返回元素给item。当我们删除元素之后,下一次循环时候会先调用hasNext是否还有元素,此时还有。在调用next方法返回该元素给item,但是调用next方法时候会检测

集合修改次数和预期修改次数是否相等,如果不等的话则抛出异常。(本例是不等的,因为移除1之后modCount=4,而expectModCount=3)。

 

那么为什么删除倒数第二个元素不会异常呢?示例代码:

复制代码
/**     *没有异常     */    public static void hasNotExcption() {        List<String> list1 = new ArrayList<String>();        list1.add("1");        list1.add("2");        list1.add("3");        list1.add("4");        for (String item : list1) {            if ("3".equals(item)) {                list1.remove(item);            }        }    }
复制代码

因为我们删除倒数第二个元素时候,此时size=size-1,当删除完元素进入下一次循环时候,此时hasNext方法判断是否还有元素的时候返回是false(hasNext使用的是游标和size进行比较)。所以就不会调用next方法了。上面例子我们知道异常就是在next方法中抛的,所以

删除倒数第二个元素就不会有异常。

 

注意:如果删除的是倒数第二个元素那么最后的结果是否正确呢?答案:显然是不正确的。

通过如下示例代码检验:

复制代码
    /**     *没有异常     */    public static void hasNotExcption() {        List<String> list1 = new ArrayList<String>();        list1.add("1");        list1.add("2");        list1.add("3");        list1.add("3");        for (String item : list1) {            if ("3".equals(item)) {                list1.remove(item);            }        }        // 显然最后一个3没有删除,所以结果错误        System.out.println(list1); //[1, 2, 3]    }
复制代码

结果是错误的,原因:因为最后一个元素由于在移除倒数第二个元素时候将size-1了,所以遍历集合提前结束了。所以没有完全移除掉所有的“3”


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


原创粉丝点击