zoukankan      html  css  js  c++  java
  • 有关集合的foreach循环里的add/remove

    转自:Hollis(微信号:hollischuang)

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

    但是手册中并没有给出具体原因,本文就来深入分析一下该规定背后的思考。

    1 .foreach循环

    foreach循环(Foreach loop)是计算机编程语言中的一种控制流程语句,通常用来循环遍历数组或集合中的元素。

    Java语言从JDK 1.5.0开始引入foreach循环。在遍历数组、集合方面,foreach为开发人员提供了极大的方便。通常也被称之为增强for循环。

    foreach 语法格式如下:

    1 for(元素类型t 元素变量x : 遍历对象obj){ 
    2      引用了x的java语句; 
    3 } 

    以下实例演示了 普通for循环 和 foreach循环使用:

     1 public static void main(String[] args) {
     2     // 使用ImmutableList初始化一个List
     3     List<String> userNames = ImmutableList.of("Hollis", "hollis", "HollisChuang", "H");
     4 
     5     System.out.println("使用for循环遍历List");
     6     for (int i = 0; i < userNames.size(); i++) {
     7         System.out.println(userNames.get(i));
     8     }
     9 
    10     System.out.println("使用foreach遍历List");
    11     for (String userName : userNames) {
    12         System.out.println(userName);
    13     }
    14 }

    以上代码运行输出结果为:

     1 使用for循环遍历List
     2 Hollis
     3 hollis
     4 HollisChuang
     5 H
     6 使用foreach遍历List
     7 Hollis
     8 hollis
     9 HollisChuang
    10 H

    可以看到,使用foreach语法遍历集合或者数组的时候,可以起到和普通for循环同样的效果,并且代码更加简洁。所以,foreach循环也通常也被称为增强for循环。

    但是,作为一个合格的程序员,我们不仅要知道什么是增强for循环,还需要知道增强for循环的原理是什么?

    其实,增强for循环也是Java给我们提供的一个语法糖,如果将以上代码编译后的class文件进行反编译(使用jad工具)的话,可以得到以下代码:

     1 Iterator iterator = userNames.iterator();
     2 do
     3 {
     4     if(!iterator.hasNext())
     5         break;
     6     String userName = (String)iterator.next();
     7     if(userName.equals("Hollis"))
     8         userNames.remove(userName);
     9 } while(true);
    10 System.out.println(userNames);

    可以发现,原本的增强for循环,其实是依赖了while循环和Iterator实现的。(请记住这种实现方式,后面会用到!)

    2 .问题重现

    规范中指出不让我们在foreach循环中对集合元素做add/remove操作,那么,我们尝试着做一下看看会发生什么问题。

     1 // 使用双括弧语法(double-brace syntax)建立并初始化一个List
     2 List<String> userNames = new ArrayList<String>() {{
     3     add("Hollis");
     4     add("hollis");
     5     add("HollisChuang");
     6     add("H");
     7 }};
     8 
     9 for (int i = 0; i < userNames.size(); i++) {
    10     if (userNames.get(i).equals("Hollis")) {
    11         userNames.remove(i);
    12     }
    13 }
    14 
    15 System.out.println(userNames);

    以上代码,首先使用双括弧语法(double-brace syntax)建立并初始化一个List,其中包含四个字符串,分别是Hollis、hollis、HollisChuang和H。

    ps 双括弧语法

    {{第一层括弧 实际是定义了一个内部匿名类 (Anonymous Inner Class),第二层括弧 实际上是一个实例初始化块 (instance initializer block),这个块在内部匿名类构造时被执行。这个块之所以被叫做“实例初始化块”是因为它们被定义在了一个类的实例范围内。这和“静态初始化块 (static initialzer)”不同,因为这种块在定义时在括弧前使用了static关键字,因此它的和类在同一个范围内的,也就是说当类加载时就会被执行,实例初始化块中可以使用其容器范围内的所有方法及变量,但特别需要注意的是实例初始化块是在构造器之前运行的。这种方法只适用于不是final的类,因为final类是无法建立内部匿名子类,好在集合类都没有这个限制。}}

    然后使用普通for循环对List进行遍历,删除List中元素内容等于Hollis的元素。然后输出List,输出结果如下:

    1 [hollis, HollisChuang, H]

    以上是使用普通的for循环在遍历的同时进行删除,那么,我们再看下,如果使用增强for循环的话会发生什么:

     1 List<String> userNames = new ArrayList<String>() {{
     2     add("Hollis");
     3     add("hollis");
     4     add("HollisChuang");
     5     add("H");
     6 }};
     7 
     8 for (String userName : userNames) {
     9     if (userName.equals("Hollis")) {
    10         userNames.remove(userName);
    11     }
    12 }
    13 
    14 System.out.println(userNames);

    以上代码,使用增强for循环遍历元素,并尝试删除其中的Hollis字符串元素。运行以上代码,会抛出以下异常:

    java.util.ConcurrentModificationException

    同样的,读者可以尝试下在增强for循环中使用add方法添加元素,结果也会同样抛出该异常。

    之所以会出现这个异常,是因为触发了一个Java集合的错误检测机制——fail-fast 。

    3.fail-fast

    接下来,我们就来分析下在增强for循环中add/remove元素的时候会抛出java.util.ConcurrentModificationException的原因,即解释下到底什么是fail-fast进制,fail-fast的原理等。

    fail-fast,即快速失败,它是Java集合的一种错误检测机制。当多个线程对集合(非fail-safe的集合类)进行结构上的改变的操作时,有可能会产生fail-fast机制,这个时候就会抛出ConcurrentModificationException(当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常)

    同时需要注意的是,即使不是多线程环境,如果单线程违反了规则,同样也有可能会抛出改异常。

    那么,在增强for循环进行元素删除,是如何违反了规则的呢?

    要分析这个问题,我们先将增强for循环这个语法糖进行解糖(使用jad对编译后的class文件进行反编译),得到以下代码:

     1 public static void main(String[] args) {
     2     // 使用ImmutableList初始化一个List
     3     List<String> userNames = new ArrayList<String>() {{
     4         add("Hollis");
     5         add("hollis");
     6         add("HollisChuang");
     7         add("H");
     8     }};
     9 
    10     Iterator iterator = userNames.iterator();
    11     do
    12     {
    13         if(!iterator.hasNext())
    14             break;
    15         String userName = (String)iterator.next();
    16         if(userName.equals("Hollis"))
    17             userNames.remove(userName);
    18     } while(true);
    19     System.out.println(userNames);
    20 }

    然后运行以上代码,同样会抛出异常。我们来看一下ConcurrentModificationException的完整堆栈:

    通过异常堆栈我们可以到,异常发生的调用链ForEachDemo的第23行,Iterator.next 调用了 Iterator.checkForComodification方法 ,而异常就是checkForComodification方法中抛出的。

    其实,经过debug后,我们可以发现,如果remove代码没有被执行过,iterator.next这一行是一直没报错的。抛异常的时机也正是remove执行之后的的那一次next方法的调用。

    我们直接看下checkForComodification方法的代码,看下抛出异常的原因:

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

    代码比较简单,modCount != expectedModCount的时候,就会抛出ConcurrentModificationException

    那么,就来看一下,remove/add 操作室如何导致modCount和expectedModCount不相等的吧。

    4.remove/add 做了什么

    首先,我们要搞清楚的是,到底modCountexpectedModCount这两个变量都是个什么东西。

    通过翻源码,我们可以发现:

    • modCount是ArrayList中的一个成员变量。它表示该集合实际被修改的次数。

    • expectedModCount 是 ArrayList中的一个内部类——Itr中的成员变量。expectedModCount表示这个迭代器期望该集合被修改的次数。其值是在ArrayList.iterator方法被调用的时候初始化的。只有通过迭代器对集合进行操作,该值才会改变。

    • Itr是一个Iterator的实现,使用ArrayList.iterator方法可以获取到的迭代器就是Itr类的实例。

    他们之间的关系如下:

     1 class ArrayList{
     2     private int modCount;
     3     public void add();
     4     public void remove();
     5     private class Itr implements Iterator<E> {
     6         int expectedModCount = modCount;
     7     }
     8     public Iterator<E> iterator() {
     9         return new Itr();
    10     }
    11 }

    其实,看到这里,大概很多人都能猜到为什么remove/add 操作之后,会导致expectedModCount和modCount不想等了。

    通过翻阅代码,我们也可以发现,remove方法核心逻辑如下:

    可以看到,它只修改了modCount,并没有对expectedModCount做任何操作。

    简单总结一下,之所以会抛出ConcurrentModificationException异常,是因为我们的代码中使用了增强for循环,而在增强for循环中,集合遍历是通过iterator进行的,但是元素的add/remove却是直接使用的集合类自己的方法。这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除/添加了,就会抛出一个异常,用来提示用户,可能发生了并发修改。

    5.正确姿势

    至此,我们介绍清楚了不能在foreach循环体中直接对集合进行add/remove操作的原因。

    但是,很多时候,我们是有需求需要过滤集合的,比如删除其中一部分元素,那么应该如何做呢?有几种方法可供参考:

    1、直接使用普通for循环进行操作

    我们说不能在foreach中进行,但是使用普通的for循环还是可以的,因为普通for循环并没有用到Iterator的遍历,所以压根就没有进行fail-fast的检验。

     1 List<String> userNames = new ArrayList<String>() {{
     2         add("Hollis");
     3         add("hollis");
     4         add("HollisChuang");
     5         add("H");
     6     }};
     7 
     8     for (int i = 0; i < 1; i++) {
     9         if (userNames.get(i).equals("Hollis")) {
    10             userNames.remove(i);
    11         }
    12     }
    13     System.out.println(userNames);

    2、直接使用Iterator进行操作

    除了直接使用普通for循环以外,我们还可以直接使用Iterator提供的remove方法。

     1 List<String> userNames = new ArrayList<String>() {{
     2         add("Hollis");
     3         add("hollis");
     4         add("HollisChuang");
     5         add("H");
     6     }};
     7 
     8     Iterator iterator = userNames.iterator();
     9 
    10     while (iterator.hasNext()) {
    11         if (iterator.next().equals("Hollis")) {
    12             iterator.remove();
    13         }
    14     }
    15     System.out.println(userNames);

    如果直接使用Iterator提供的remove方法,那么就可以修改到expectedModCount的值。那么就不会再抛出异常了。其实现代码如下:

    3、使用Java 8中提供的filter过滤

    Java 8中可以把集合转换成流,对于流有一种filter操作, 可以对原始 Stream 进行某项测试,通过测试的元素被留下来生成一个新 Stream。

    1     List<String> userNames = new ArrayList<String>() {{
    2         add("Hollis");
    3         add("hollis");
    4         add("HollisChuang");
    5         add("H");
    6     }};
    7 
    8     userNames = userNames.stream().filter(userName -> !userName.equals("Hollis")).collect(Collectors.toList());
    9     System.out.println(userNames);

    4、直接使用fail-safe的集合类

    在Java中,除了一些普通的集合类以外,还有一些采用了fail-safe机制的集合类。这样的集合容器在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

    由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。

     1 ConcurrentLinkedDeque<String> userNames = new ConcurrentLinkedDeque<String>() {{
     2     add("Hollis");
     3     add("hollis");
     4     add("HollisChuang");
     5     add("H");
     6 }};
     7 
     8 for (String userName : userNames) {
     9     if (userName.equals("Hollis")) {
    10         userNames.remove();
    11     }
    12 }

    基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

    java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

    5、使用增强for循环其实也可以

    如果,我们非常确定在一个集合中,某个即将删除的元素只包含一个的话, 比如对Set进行操作,那么其实也是可以使用增强for循环的,只要在删除之后,立刻结束循环体,不要再继续进行遍历就可以了,也就是说不让代码执行到下一次的next方法。

     1   List<String> userNames = new ArrayList<String>() {{
     2         add("Hollis");
     3         add("hollis");
     4         add("HollisChuang");
     5         add("H");
     6     }};
     7 
     8     for (String userName : userNames) {
     9         if (userName.equals("Hollis")) {
    10             userNames.remove(userName);
    11             break;
    12         }
    13     }
    14     System.out.println(userNames);

    以上这五种方式都可以避免触发fail-fast机制,避免抛出异常。如果是并发场景,建议使用concurrent包中的容器,如果是单线程场景,Java8之前的代码中,建议使用Iterator进行元素删除,Java8及更新的版本中,可以考虑使用Stream及filter。

    6.总结

    我们使用的增强for循环,其实是Java提供的语法糖,其实现原理是借助Iterator进行元素的遍历。

    但是如果在遍历过程中,不通过Iterator,而是通过集合类自身的方法对集合进行添加/删除操作。那么在Iterator进行下一次的遍历时,经检测发现有一次集合的修改操作并未通过自身进行,那么可能是发生了并发被其他线程执行的,这时候就会抛出异常,来提示用户可能发生了并发修改,这就是所谓的fail-fast机制。

    当然还是有很多种方法可以解决这类问题的。比如使用普通for循环、使用Iterator进行元素删除、使用Stream的filter、使用fail-safe的类等。

  • 相关阅读:
    java+opencv实现图像灰度化
    java实现高斯平滑
    hdu 3415 单调队列
    POJ 3368 Frequent values 线段树区间合并
    UVA 11795 Mega Man's Mission 状态DP
    UVA 11552 Fewest Flops DP
    UVA 10534 Wavio Sequence DP LIS
    UVA 1424 uvalive 4256 Salesmen 简单DP
    UVA 1099 uvalive 4794 Sharing Chocolate 状态DP
    UVA 1169uvalive 3983 Robotruck 单调队列优化DP
  • 原文地址:https://www.cnblogs.com/itworkerlittlewrite/p/10490192.html
Copyright © 2011-2022 走看看