在foreach里面如果我们进行remove和add会发生什么大事呢?

相信大家肯定都看过阿里巴巴开发手册,而在阿里巴巴开发手册中明确的指出,不要再foreach循环里面进行元素的add和remove,如果你非要进行remove元素,那么请使用Iterator方式,如果存在并发,那么你一定要选择加锁。

典型错误实例

1
2
3
4
5
6
7
8
9
10
        List<String> list = new ArrayList<>();
        list.add("AA");
        list.add("DD");
        for (String str :list) {
            if ("AA".equals(str)){
                list.remove(str);
            }
        }
        System.out.println(list);

相信大家执行这个代码的时候没有什么感觉,因为如果你把第一个放进去,执行的时候完全没有任何的问题,大家看结果。

1
2
3
[DD]

Process finished with exit code 0

这是不是和大家想象的内容一模一样,但是大家知道如果我们把 “DD”进行remove的话,会出现什么呢?

1
2
3
4
5
6
7
8
9
  List<String> list = new ArrayList<>();
        list.add("AA");
        list.add("DD");
        for (String str :list) {
            if ("DD".equals(str)){
                list.remove(str);
            }
        }
        System.out.println(list);

先给大家看结果:

1
2
3
4
5
6
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.chuyikeji.jsoup.controller.TestClass.main(TestClass.java:25)

Process finished with exit code 1

对,你没有看错,错了,竟然出错了,这时候为什么呢?

为什么会出现异常

我们直接从异常信息入手,java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)

1
2
3
4
 final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

异常信息中的909行也就是从这里开始的,大家肯定会想他在比较两个值 modCountexpectedModCount,那么这两个变量是什么呢?

其实说白了,他们就是来表示修改次数的变量,其中modCount表示集合的修改次数,这其中包括了调用集合本身的add方法等修改方法时进行的修改和调用集合迭代器的修改方法进行的修改。而expectedModCount则是表示迭代器对集合进行修改的次数。

这话一说,心里是不是直接一个大写的 WC?迭代器,竟然还是迭代器?在这里阿粉就不再详细的去给大家说这两个变量是个什么东西了,大家有兴趣的可以去查看源码一下 AbstractList的601行之前的注释,

而 expectedModCount 是个什么鬼?从ArrayList 源码可知,这个变量是一个局部变量,也就是说每个方法内部都有expectedModCount 和 modCount 的判断机制,进一步来讲,这个变量就是 预期的修改次数,而这个判断机制,很多人都会直接告诉你说fail-fas机制,你说这个机制,但是很多人都知道,你想解释明白他,需要点功夫的,理解起来肯定更需要花点时间的。

我们在这里也搞搞事,直接给他反编译一下我们的这个 Class,看看是个什么东西弄得我们这么头疼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws Exception {
        List<String> list = new ArrayList();
        list.add("AA");
        list.add("DD");
        Iterator var2 = list.iterator();

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

        System.out.println(list);
    }

看里面使用的也是迭代器,也就是说,其实 foreach 每次循环都调用了一次iterator的next()方法,这也是为什么我们从堆栈信息看到的原因。

而这时候我们就可以这么理解了,我们第一次迭代的时候 AA != DD ,直接迭代第二次,这时候就相等了,执行remove()方法,这时候就是modCount++,再次调用next()的时候,modCount != expectedModCount 这个就不成立了,所以异常信息出现了,其实也可以理解为在 hasNext() 里面,cursor != size 而这时候就会出现错误了。

也就是说 remove方法它只修改了modCount,并没有对expectedModCount做任何操作。add同理,所以他是不相等的所以会抛出异常。

怎么避免这个问题的出现呢?

为什么阿里巴巴的规范手册会这样子定义么?

它为什么推荐我们使用 Iterator呢?

直接使用迭代器会修改expectedModCount,而我们使用foreach的时候,remove方法它只修改了modCount,并没有对expectedModCount做任何操作,而Iterator就不会这个样子,

1
2
3
4
5
6
7
8
9
10
11
  List<String> list = new ArrayList<>();
        list.add("AA");
        list.add("DD");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()){
            if(iterator.next().equals("DD")){
                iterator.remove();
            }
        }
        System.out.println(list);
    }

结果如下:

1
2
3
[AA]

Process finished with exit code 0

而这种方法很好用,但是很多大佬就说,你直接用Java8的新特性不香么?是呀,那么我们直接用 Java8 再来试试

1
2
3
4
5
6
7
List<String> list = new ArrayList<>();
        list.add("AA");
        list.add("DD");

        List<String> dd = list.stream().filter(s -> !s.equals("DD")).collect(Collectors.toList());

        System.out.println(dd);

果然不得不说,真香定理永恒存在,关于为什么不能remove和add你学会了么?

Java Geek Tech wechat
欢迎订阅 Java 极客技术,这里分享关于 Java 的一切。