我要在for循环List中删除元素

8,922 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

for循环可以删除集合元素吗,往往我们得到的答案有时候就是不可以,安全起见,要迭代器,包括我在阿里的开发规范里也写了这么一句话, 不要在 foreach 循环里进行元素的 remove / add 操作。remove 元素请使用 iterator 方式,如果并发操作,需要对 iterator 对象加锁 

当然,他说的可以是怕某些人对下面的我的方法的微操有不注意的地方,所以不如一开始就说不可以。

依然记得刚来第三天写个接口我就for循环内删除元素,当时很沙雕,恰好又被代码走查看到了,尴尬的我挖了个洞将for改成了迭代器方式遍历,这两天看个大佬的代码,他就是for循环并remove其中元素,我开心的以为发现了一个bug,嗯,再往下看不对,这代码妙啊,百度了一下,有了这篇文章 下面我们通过几个例子以及分析源码的方式来看看问题,nice

问题一

List<String> 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值被修改了,所以抛出异常,我想到了这里,大家应该看出问题的所在了,

  1. 一个是删除后元素位置前挪了导致连续相等的元素判断不到
  2. 一个是删除元素后改动的次数变得和期望变动的次数不一样了导致的这些异常信息

解决及规避

好了,既然知道问题的原因了,那么我们该怎么规避呢???

对于第一个问题,既然删除后它会前移,那我逆序遍历是不是就好了呢(逻辑鬼才),毕竟我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<String> 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,具体可以参考ArrayList线程不安全,JUC是如何处理的

防止有些人不看,我贴张图

咦,加了个ReentrantLock,怪不得呢,其他的我觉得和上面的for都是异曲同工了,

好啦,今天的不开心就止于此吧,明天依旧光芒万丈发啊,宝贝!

撒花,完结!!!

一个没有梦想的梦想家