循环删除list中的元素使用for循环的方式是有问题的,下面就来讲一讲。。
背景:业务中经常会涉及遍历list时对集合进行插入或者删除操作
一、 错误方式
先看看下面几段代码,1是foreach的方式去遍历list并删除元素,2是用迭代器的方式遍历list并删除元素,3是下标遍历
1. foreach
public void testDel(){ List<Integer> list = Lists.newArrayList(); for(int i=1;i<=5;i++){ list.add(i); } for(Integer ele : list){ if(ele == 3) list.remove(ele); } }
2. Iterator
public void testDel(){ List<Integer> list = Lists.newArrayList(); for(int i=1;i<=5;i++){ list.add(i); } Iterator<Integer> iterator = list.iterator(); while(iterator.hasNext()){ Integer integer = iterator.next(); if(integer==3) list.remove(integer); } }
以上两段代码的执行结果都是 java.util.ConcurrentModificationException。
在使用ArrayList时,当尝试用foreach或者Iterator遍历集合时进行删除或者插入元素的操作时,会抛出这样的异常:java.util.ConcurrentModificationException
关于这个异常的原因,看了很多文章,基本上解释如下:ArrayList的父类AbstarctList中有一个域modCount,每次对集合进行修改(增添、删除元素)时modCount都会+1。
而foreach的实现原理就是迭代器Iterator,在这里,迭代ArrayList的Iterator中有一个变量expectedModCount,该变量会初始化和modCount相等,但当对集合进行插入,删除操作,modCount会改变,就会造成expectedModCount!=modCount,此时就会抛出java.util.ConcurrentModificationException异常,是在checkForComodification方法中,代码如下:
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
那么,以上两种方式都不用呢,用下标遍历呢?看如下代码:
3 . 下标遍历
public void testDel() { List<Integer> list = Lists.newArrayList(); list.add(1); list.add(2); list.add(2); list.add(2); list.add(2); for(int i=0;i<list.size();i++){ if(list.get(i)==2){ list.remove(i); } } }
//结果: list = [1,2,2]
因为下标是固定死的自增,但list的大小在随着删除元素不停的减小,并且后面的元素往前移了1位,所以后面的元素遍历不到。在i=3时,由于删了2个元素,size=3,所以循环直接结束。因此这种方式也是不符合预期的。
二、解决方法
但业务中经常涉及遍历时删除或插入操作。所以如何安全的操作ArrayList呢,解决方法如下!
1.使用迭代器的remove方法
在使用迭代器遍历时,可使用迭代器的remove方法,因为Iterator的remove方法中 有如下的操作:
expectedModCount = modCount;
所以避免了ConcurrentModificationException的异常。代码如下:
public void testDel() { List<Integer> list = Lists.newArrayList(); list.add(1); list.add(2); list.add(2); list.add(2); Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { Integer integer = iterator.next(); if (integer == 2) iterator.remove(); } }
执行后结果: list = [1]
其实在阿里巴巴Java开发手册中原话:不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。
2.倒序遍历删除
public void testDel() { List<Integer> list = Lists.newArrayList(); list.add(1); list.add(2); list.add(2); list.add(2); list.add(2); for(int i=list.size()-1;i>=0;i--){ if(list.get(i)==2){ list.remove(i); } } }
结果: list = [1]
可见,也达到了预期的效果。因为每次删除一个元素,list大小-1,但是倒序,循环条件为i>=0,所以list的size改变并没有对遍历造成影响,且元素的前移也不会对倒序遍历有影响。
其实JAVA中循环遍历list有三种方式for循环、增强for循环(也就是常说的foreach循环)、iterator遍历。
1、for循环遍历list
for(int i=0;i<list.size();i++){ if(list.get(i).equals("del")) list.remove(i); }
这种方式的问题在于,删除某个元素后,list的大小发生了变化,而你的索引也在变化,所以会导致你在遍历的时候漏掉某些元素。比如当你删除第1个元素后,继续根据索引访问第2个元素时,因为删除的关系后面的元素都往前移动了一位,所以实际访问的是第3个元素。因此,这种方式可以用在删除特定的一个元素时使用,但不适合循环删除多个元素时使用。
2、增强for循环
for(String x:list){ if(x.equals("del")) list.remove(x); }
这种方式的问题在于,删除元素后继续循环会报错误信息ConcurrentModificationException,因为元素在使用的时候发生了并发的修改,导致异常抛出。但是删除完毕马上使用break跳出,则不会触发报错。
3、iterator遍历
Iterator<String> it = list.iterator(); while(it.hasNext()){ String x = it.next(); if(x.equals("del")){ it.remove(); } }
这种方式可以正常的循环及删除。但要注意的是,使用iterator的remove方法,如果用list的remove方法同样会报上面提到的ConcurrentModificationException错误。
总结:
(1)循环删除list中特定一个元素的,可以使用三种方式中的任意一种,但在使用中要注意上面分析的各个问题。
(2)循环删除list中多个元素的,应该使用迭代器iterator方式。
所以在对list或者hashmap遍历时候进行元素删增操作时,一定要验证下,我开始想当然的以为OK,调试才发现前面几种方式是有问题的,不是异常,就是效果没达到预期。到线上会出大问题。所以整理了下,推荐最后两种方式!