增强for循环

784 阅读5分钟

增强for循环

原代码

public static  void testForEach(){
    List<String> userNames = new ArrayList<String>() {{
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    for (String userName : userNames) {
        if (userName.equals("Hollis")) {
            userNames.remove(userName);
        }
    }
    System.out.println(userNames);
}

反编译

public static void testForEach() {
    List<String> userNames = new ArrayList<String>() {
        {
            this.add("Hollis");
            this.add("hollis");
            this.add("HollisChuang");
            this.add("H");
        }
    };
    Iterator var1 = userNames.iterator();

    while(var1.hasNext()) {
        String userName = (String)var1.next();
        if (userName.equals("Hollis")) {
            userNames.remove(userName);
        }
    }

    System.out.println(userNames);
}

其实JAVA中的增强for循环底层是通过迭代器模式来实现的。

fail-fast

既然增强for循环通过迭代器实现,那么必然有迭代器的特性。在Java中的fail-fast机制中有介绍过。在使用迭代器遍历元素的时候,在对集合进行删除的时候一定要注意,使用不当有可能发生ConcurrentModificationException,如以下代码:

    String userName = (String)var1.next();
        if (userName.equals("Hollis")) {
            userNames.remove(userName);
   }

接下来,我们就来分析下在增强for循环中add/remove元素的时候会抛出java.util.ConcurrentModificationException的原因,即解释下到底什么是fail-fast进制,fail-fast的原理等。
fail-fast,即快速失败,它是Java集合的一种错误检测机制。当多个线程对集合(非fail-safe的集合类)进行结构上的改变的操作时,有可能会产生fail-fast机制,这个时候就会抛出ConcurrentModificationException(当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常)。

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

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

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

public static void main(String[] args) {\
    // 使用ImmutableList初始化一个List\
    List<String> userNames = new ArrayList<String>() {{\
        add("Hollis");\
        add("hollis");\
        add("HollisChuang");\
        add("H");\
    }};\
\
    Iterator iterator = userNames.iterator();\
    do\
    {\
        if(!iterator.hasNext())\
            break;\
        String userName = (String)iterator.next();\
        if(userName.equals("Hollis"))\
            userNames.remove(userName);\
    } while(true);\
    System.out.println(userNames);\
}

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

3a5b837d14468ae824762937425c5ff8.jpg

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

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

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

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

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

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

remove/add 做了什么

首先,我们要搞清楚的是,到底modCount和expectedModCount这两个变量都是个什么东西。 通过翻源码,我们可以发现:

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

他们之间的关系如下:

class ArrayList{\
    private int modCount;\
    public void add();\
    public void remove();\
    private class Itr implements Iterator<E> {\
        int expectedModCount = modCount;\
    }\
    public Iterator<E> iterator() {\
        return new Itr();\
    }\
}

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

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

83fd283c678668228ef627de127431ed.jpg

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

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

正确姿势

至此,我们介绍清楚了不能在foreach循环体中直接对集合进行add/remove操作的原因。
但是,很多时候,我们是有需求需要过滤集合的,比如删除其中一部分元素,那么应该如何做呢?有几种方法可供参考:

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

List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

Iterator iterator = userNames.iterator();

while (iterator.hasNext()) {
    if (iterator.next().equals("Hollis")) {
        //使用迭代器进行remove
        iterator.remove();
    }
}
System.out.println(userNames);

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

56137a03da966053dcf10a5507f0a73d.jpg

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

List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

userNames = userNames.stream().filter(userName -> !userName.equals("Hollis")).collect(Collectors.toList());
System.out.println(userNames);

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

List<String> userNames = new ArrayList<String>() {{\
        add("Hollis");\
        add("hollis");\
        add("HollisChuang");\
        add("H");\
    }};\
\
    for (String userName : userNames) {\
        if (userName.equals("Hollis")) {\
            userNames.remove(userName);\
            break;\
        }\
    }\
    System.out.println(userNames);

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