Java中使用增强for循环遍历List, remove或add元素出现异常的总结

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方法。

 

 

 

 

 

 

 

 

  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值