foreach循环是计算机编程语言中的一种流程控制语句, 一般用来循环遍历数组跟集合, 获取里面的元素; Java从JDK 1.5开始引入foreach循环, 在遍历集合跟数组, 十分的方便, 也叫作增强for;
foreach的语法格式如下图所示
其遍历List的代码如下图所示:
输出的结果为
可以从代码看跟输出结果看出来, 使用foreach遍历集合或者数组的时候, 可以起到普通for循环同样的效果, 并且代码格式上更加整洁; 但是作为程序开发人员来说, 只是知道增强for的使用还是不够的, 防止知其然而不知其所以然这样的尴尬局面; 除了熟练应用以外, 更加重要的是要熟悉其原理; 增强for循环是Java提供的一个语法糖(语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会), 将以上代码的class文件反编译后可以看到源码:
很明显, 所谓的增强for其实是依赖了while循环和Iterator实现的, 那么重点来了, 尝试在foreach循环中对list的元素进行add和moved操作的时候会发生什么?
首先使用双括弧语法(duble-brace syntax)建立并初始化一个List, 其中包含四个字符串, 分别是Hollis, hollis, HollisChuang, H
然后使用普通for循环对List进行遍历, 删除List中元素为Hollis的元素, 然后输出List, 结果如下:
这是使用普通for循环对List中的元素进行操作, 结果输出很正常, 那么再验证一下使用增强for进行操作会是什么结果?
使用增强for遍历操作List, 会抛出以下异常:
之所以会抛出这个异常的原因是因为该操作方法触发了一个Java集合的错误检测机制: fail-fast;
fail-fast, 即快速失败, 它是Java集合的一种错误检测机制; 当多个线程对集合(非fail-safe的集合类)进行结构上的操作的时候, 就有可能产生fail-fast机制, 这个时候就会抛出ConcurrentModificationException(当方法检测到对象的并发修改, 但不允许这种修改时就抛出该异常); 即使在非多线程环境, 单线程操作中违反了规则, 同样也会有可能抛出该异常;
运行以上代码,同样会抛出异常。看一下ConcurrentModificationException的完整堆栈:
通过异常堆栈可以看到,异常发生的调用链ForEachDemo的第23行,Iterator.nextI
调用了 Iterator.checkForComodification
方法 ,而异常就是checkForComodification方法中抛出的。其实,经过debug后,可以发现,如果remove代码没有被执行过,iterator.next这一行是一直没报错的。抛异常的时机也正是remove执行之后的的那一次next方法的调用。
checkForComodification方法的代码,看下抛出异常的原因:
代码很明显, 当modCount != expectedModCount的时候, 就会抛出异常
通过翻源码,可以发现:modCount是ArrayList中的一个成员变量。它表示该集合实际被修改的次数。expectedModCount 是 ArrayList中的一个内部类——Itr中的成员变量。expectedModCount表示这个迭代器期望该集合被修改的次数。其值是在ArrayList.iterator方法被调用的时候初始化的。只有通过迭代器对集合进行操作,该值才会改变。Itr是一个Iterator的实现,使用ArrayList.iterator方法可以获取到的迭代器就是Itr类的实例。
之间的关系如下
再往下看, remove方法的核心逻辑如下
它只修改了modCount,并没有对expectedModCount做任何操作。之所以会抛出ConcurrentModificationException异常,是因为代码中使用了增强for循环,而在增强for循环中,集合遍历是通过iterator进行的,但是元素的add/remove却是直接使用的集合类自己的方法。这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除/添加了,就会抛出一个异常,用来提示用户,可能发生了并发修改。
那么开发过程中遇到此类场景, 应该如何去正确的使用循环?
1,
直接使用普通for循环进行操作
不能在foreach中进行,但是使用普通的for循环还是可以的,因为普通for循环并没有用到Iterator的遍历,所以压根就没有进行fail-fast的检验。
2,
直接使用Iterator进行操作
除了直接使用普通for循环以外,我们还可以直接使用Iterator提供的remove方法。
如果直接使用Iterator提供的remove方法,那么就可以修改到expectedModCount的值。那么就不会再抛出异常了。其实现代码如下:
3,
直接使用fail-safe的集合类
在Java中,除了一些普通的集合类以外,还有一些采用了fail-safe机制的集合类。这样的集合容器在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。
基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
4,
使用增强for循环其实也可以
如果,我们非常确定在一个集合中,某个即将删除的元素只包含一个的话, 比如对Set进行操作,那么其实也是可以使用增强for循环的,只要在删除之后,立刻结束循环体,不要再继续进行遍历就可以了,也就是说不让代码执行到下一次的next方法。