Java集合系列之集合操作的坑【五】

1. 在 foreach 循环里进行元素的 remove/add 操作

在之前的一篇文章中:阿里巴巴Java开发手册阅读总结,总结了禁止在 foreach 循环里进行元素的 remove/add 操作,文章中只是简单总结了一下fail-fast机制。这里深入分析一下。

1.1 fail-fast机制

百度百科中的解释:

fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
要了解fail-fast机制,我们首先要对ConcurrentModificationException 异常有所了解。当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常。同时需要注意的是,该异常不会始终指出对象已经由不同线程并发修改,如果单线程违反了规则,同样也有可能会抛出该异常。
诚然,迭代器的快速失败行为无法得到保证,它不能保证一定会出现该错误,但是快速失败操作会尽最大努力抛出ConcurrentModificationException异常,所以因此,为提高此类操作的正确性而编写一个依赖于此异常的程序是错误的做法,正确做法是:ConcurrentModificationException 应该仅用于检测 bug。

Java的部分集合类中运用了fail-fast机制进行设计,一旦使用不当,触发了fail-fast机制设计的代码,就会发生非预期情况。

Java中的fail-fast机制,默认指的是Java集合的一种错误检测机制。

如果在foreach 循环里对某些集合元素进行元素的 remove/add 操作的时候,一般情况下都会触发fail-fast机制,进而抛出CMException。

foreach其实是一种语法糖,其实是依赖了while循环和Iterator实现的,所有的Collecation集合类都会实现Iterable接口。推进阅读 语法糖,

查看源代码来分析异常原因:

ArrayList的interator()方法:
在这里插入图片描述
Java集合系列之集合操作的坑【五】_第1张图片
迭代器的本质是先调用hasNext()方法判断存不存在下一元素,然后再用next()方法取下一元素。next()方法中先调用checkForComodification()方法,在该方法中对modCount和expectedModCount进行了比较,如果二者不想等,则抛出CMException。

  1. modCount是ArrayList父类AbstractList中的一个成员变量。它表示该集合实际被修改的次数。初始值为0.
    Java集合系列之集合操作的坑【五】_第2张图片
  2. expectedModCount 是 ArrayList中的一个内部类——Itr中的成员变量。其值随着Itr被创建而初始化。只有通过迭代器对集合进行操作,该值才会改变。

ArrayList的add()方法和remove()方法:
Java集合系列之集合操作的坑【五】_第3张图片
Java集合系列之集合操作的坑【五】_第4张图片
可以看到remove方法和add方法只修改了modCount,并没有对expectedModCount做任何操作。

所以在调用迭代器的next()方法时会抛出ConcurrentModificationException。

在使用Java的集合类的时候,如果发生ConcurrentModificationException,优先考虑fail-fast的情况,实际上并没有真的发生并发,只是Iterator使用了fail-fast的保护机制,只要他发现有某一次修改是未经过自己进行的,那么就会抛出异常。

1.2 特殊情况

其实存在一种特殊情况,先看下面一段代码:

package com.aecc.busys.xamh;

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

public class Test1 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
//        Iterator iterator = list.iterator();
//        while (iterator.hasNext()){
//            String next = iterator.next();
//            if(next.equals("1")){
//                iterator.remove();
//            }
//        }

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

运行并没有报错,可是如果把1换成2,就会报错:Java集合系列之集合操作的坑【五】_第5张图片
因为删除倒数第二个元素后,再遍历的时候,cursor为size-1,size也减了1,所以cursor = size;退出循环,删除成功。

原因:Java集合系列之集合操作的坑【五】_第6张图片

结论是:如果foreach循环体中删除的是倒数第二个元素,则不会有问题。如果是其它的元素则会导致expectedModCount 和 modCount不一致(Itr类中的expectedModCount保持不变),导致
ConcurrentModificationException异常。

1.3 fail-safe

为了避免触发fail-fast机制导致异常,可以使用一些采用了fail-safe机制的集合类。

fail-save的集合容器在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。并且其中的修改方法,如add/remove都是通过加锁来控制并发的。

java.util.concurrent包下的容器都是fail-safe的,可以在多线程下并发使用,并发修改。同时也可以在foreach中进行add/remove 。

所以,CopyOnWriteArrayList中的迭代器在迭代的过程中不需要做fail-fast的并发检测。

但是,虽然CopyOnWriteArrayList避免了ConcurrentModificationException,但迭代器并不能访问到修改后的内容。如以下代码:

package com.linyf.demo;

import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListTest {
    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<String>() {{
            add("a");
            add("b");
            add("c");
            add("d");
        }};

        Iterator it = list.iterator();

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

        System.out.println(list);

        while(it.hasNext()){
            System.out.println(it.next());
        }
    }
}

执行结果:
Java集合系列之集合操作的坑【五】_第7张图片
推荐阅读:Java集合系列之线程安全的单列集合【七】

2. Arrays.asList()不能进行结构化修改

Arrays.asList()得到的集合本质是一个数组,所以容量是固定的,不能进行增加和删除操作。否则会报UnsupportedOperationException:
Java集合系列之集合操作的坑【五】_第8张图片
可以这样操作,用ArrayList进行接收:
Java集合系列之集合操作的坑【五】_第9张图片

3. 谨慎使用List的subList方法

List的subList方法并没有创建一个新的List,而是使用了原List的视图,这个视图使用内部类SubList表示。

所以,不能把subList方法返回的List强制转换成ArrayList等类,因为他们之间没有继承关系。

另外,视图和原List的修改还需要注意几点,尤其是他们之间的相互影响:

  1. 对父(sourceList)子(subList)List做的非结构性修改(non-structural changes),都会影响到彼此。
  2. 对子List做结构性修改,操作同样会反映到父List上。
  3. 对父List做结构性修改,会抛出异常ConcurrentModificationException。

所以,阿里巴巴Java开发手册中有另外一条规定:
在subList使用场景中高度注意对源集合的结构性修改(添加或删除),均会导致对subList进行查询 , 增加 , 删除都会产生ConcurrentModificationException异常

public static void main2(String[] args) {
        List<String> names = new ArrayList<String>() {{
            add("a");
            add("b");
            add("c");
        }};

        List<String> subList = names.subList(0, 1);
        System.out.println(subList);
    }

    //非结构性修改subLis---OK
    public static void main3(String[] args) {
        List<String> sourceList = new ArrayList<String>() {{
            add("a");
            add("b");
            add("c");
            add("d");
            add("e");
            add("f");
        }};

        List subList = sourceList.subList(2, 5);

        System.out.println("sourceList : " + sourceList);
        System.out.println("sourceList.subList(2, 5) 得到List :");
        System.out.println("subList : " + subList);

        subList.set(1, "666");

        System.out.println("subList.set(3,666) 得到List :");
        System.out.println("subList : " + subList);
        System.out.println("sourceList : " + sourceList);
    }

    //非结构性修改sourceList---OK
    public static void main4(String[] args) {
        List<String> sourceList = new ArrayList<String>() {{
            add("a");
            add("b");
            add("c");
            add("d");
            add("e");
            add("f");
        }};

        List subList = sourceList.subList(2, 5);

        System.out.println("sourceList : " + sourceList);
        System.out.println("sourceList.subList(2, 5) 得到List :");
        System.out.println("subList : " + subList);

        sourceList.set(4, "666");

        System.out.println("sourceList.set(4,666) 得到List :");
        System.out.println("subList : " + subList);
        System.out.println("sourceList : " + sourceList);
    }

    //结构性修改subList---OK
    public static void main5(String[] args) {
        List<String> sourceList = new ArrayList<String>() {{
            add("a");
            add("b");
            add("c");
            add("d");
            add("e");
            add("f");
        }};

        List subList = sourceList.subList(2, 5);

        System.out.println("sourceList : " + sourceList);
        System.out.println("sourceList.subList(2, 5) 得到List :");
        System.out.println("subList : " + subList);

        subList.add("666");

        System.out.println("subList.add(666) 得到List :");
        System.out.println("subList : " + subList);
        System.out.println("sourceList : " + sourceList);

    }

    //结构性修改sourceList --------对subList进行查询 , 增加 , 删除都会产生ConcurrentModificationException异常
    public static void main(String[] args) {
        List<String> sourceList = new ArrayList<String>() {{
            add("a");
            add("b");
            add("c");
            add("d");
            add("e");
            add("f");
        }};

        List subList = sourceList.subList(2, 5);

        System.out.println("sourceList : " + sourceList);
        System.out.println("sourceList.subList(2, 5) 得到List :");
        System.out.println("subList : " + subList);

        sourceList.add("666");

        System.out.println("sourceList.add(666) 得到List :");
        System.out.println("sourceList : " + sourceList);
        System.out.println("subList : " + subList);

        //如果需要对subList作出修改,又不想动原list。那么可以创建subList的一个拷贝:
// 方法一       subList = Lists.newArrayList(subList);
//   方法二      subList.stream().skip(strart).limit(end).collect(Collectors.toList());
    }

你可能感兴趣的:(java)