for循环可以删除集合元素吗,往往我们得到的答案有时候就是不可以,安全起见,要迭代器,包括我在阿里的开发规范里也写了这么一句话, 不要在 foreach 循环里进行元素的 remove / add 操作。remove 元素请使用 iterator 方式,如果并发操作,需要对 iterator 对象加锁
当然,他说的可以是怕某些人对下面的我的方法的微操有不注意的地方,所以不如一开始就说不可以。
依然记得刚来第三天写个接口我就for循环内删除元素,当时很沙雕,恰好又被代码走查看到了,尴尬的我挖了个洞将for改成了迭代器方式遍历,这两天看个大佬的代码,他就是for循环并remove其中元素,我开心的以为发现了一个bug,嗯,再往下看不对,这代码妙啊,百度了一下,有了这篇文章 下面我们通过几个例子以及分析源码的方式来看看问题,nice
List list = new ArrayList();
list.add("111");
list.add("222");
list.add("222");
list.add("333");
list.add("222");
list.add("555");
//list.stream().forEach(System.out::println);
for(int i = 0;i < list.size();i++){
if(StrUtil.equals("222",list.get(i))){
list.remove(i);
}}
复制代码
我们先看下上面这个用例,这个结果是啥呢?是111 222 333 555,咦,明明等于222的移除了啊,怎么没移掉,而且还没报错,通常我们移除元素会报错呀,其实这种for方法在我们循环遍历的时候list.remove(i);会删除对应的元素不会报错,但是呢,删除的元素位置会空出来,后面的元素会往前移一位,这样如果有两个元素的位置是连续的话,那么后面这个元素是不会进行判断的,这样就不会符合我们的分析场景的,
这里我们点进remove方法中会发现一个rangeCheck方法,它会先检查给定的索引是否在范围内
我们按代码顺序翻一下,索引在范围内,则获取remove的元素,然后将list的元素大小减一,如果还存在,就进行元素的copy,从源数组的index+1位置开始要复制的数组元素的数量numMoved,到目标数组的指定位置,然后通过GC将最后一个位置内存回收,哦。原来是这样的,至于说的报错我们下面在分析
for (String ll : list) {
if(StrUtil.equals(ll,"333")){
list.remove(ll);
}}
复制代码
如上代码,当我们使用foreach的时候我们需要remove的是一个对象,而不是for时的下标,这里会报错java.util.ConcurrentModificationException,这就是我们说的**报错**了,我先把结果说了吧,这里我们删除元素的话其实并不会报错,报错的是for循环哪里,在你remove后下一次遍历的时候才会报错,报异常的方法是java.util.ArrayList$Itr.checkForComodification,一看就是方法里的迭代器报错,下面我们看看为啥???
我们先看这里的for循环在做啥
这里有两个变量,cursor:下一个元素索引,lastRet:上一个元素索引,
复制代码
刚第一次进来的时候会将修改的次数赋值给expectedModCount,然后执行下面的next方法,返回遍历的值,当匹配上需要remove的时候我们看下remove方法,到ArrayList的remove方法瞅瞅;
我们看到匹配上执行fastRemove(index);
这里会给modCount++;**modCount修改的次数这里加一了哈,**然后System.arraycopy赋值,执行完后,返回true,继续遍历,再进到next的方法里遍历时执行 checkForComodification();点进去看
如果他们不相等则抛出异常,而刚刚我们remove的时候modCount值被修改了,所以抛出异常,我想到了这里,大家应该看出问题的所在了,
好了,既然知道问题的原因了,那么我们该怎么规避呢???
对于第一个问题,既然删除后它会前移,那我逆序遍历是不是就好了呢(逻辑鬼才),毕竟我for的size是动态变化的
for(int i = list.size()-1;i>=0;i--){
if(StrUtil.equals("222",list.get(i))){
String remove = list.remove(i);
System.out.println("shanchu"+ remove);
}}
复制代码
这个结果是啥;不用我说了吧**shanchu222 shanchu222 shanchu222 111 333 555,**你看完美解决问题,这是一个解决办法,所以逆序的情况你不必太在意;
对于第二种情况
我们用迭代器看看
Iterator iterator = list.iterator();
while (iterator.hasNext()){
if(StrUtil.equals("222",iterator.next())){
list.remove(iterator.next());
}}
复制代码
它的expectedModCount是初始的6,对于list的remove这里依然会调用上面说的remove对象的方法,所以依然报错毫无疑问,我们就暂不理会,我们接着看(这里我们注意一下iterator.next()这个方法,这个其实就是for循环里为我们做的遍历的处理一样,只不过for循环本身为我们做了,可以上翻next方法),好了,接下来我们看看迭代器iterator为我们提供的remove方法,在我们执行remove方法的时候我们看看它做了什么
它依然会调用checkForComodification()进行判断,(这里我要说明一下这个remove方法依然是ArrayList类里的private class Itr implements Iterator 类里的方法)然后执行ArrayList.this.remove(lastRet);这里的lastRet是上面的cursor赋的值,由于这里是正序的,remove会将元素向左移动所以cursor会被从下一个值拽回来到lastRet的位置,lastRet给-1;然后将expectedModCount = modCount;咦,这里就是将期望修改次数的值和修改次数又同步起来了不是;
不知道这里你们发现没有,其实迭代器的remove方法和我们最上面的用例删除元素下标调用的方法一样,只不过迭代器后面又跟了点东西;比如删除后遍历的下标前移,修改的次数同步
好啦好啦,解决方法也写了,大家应该知道原因了,并且知道解决办法了,我觉得看完了就知道知其然知其所以然了,
对于线程安全的情况,如果想要保证线程安全,我们可以使用CopyOnWriteArrayList
防止有些人不看,我贴张图
咦,加了个ReentrantLock,怪不得呢,其他的我觉得和上面的for都是异曲同工了,