一场在循环中删除list中的元素引发的血案

起因

最近接到一个需求(假的),从一个list中的member对象中,将年龄小于18岁的对象去掉。不然展示出去之后就要出问题。

拿到需求后一顿操作,很快就写好了。逻辑也很清晰明了,大致代码如下:

void removeMember(List<Member> list) {
    for (int i = 0; i < list.size(); i++) {
        if(list.get(i).getAge()<18){
            list.remove(i);
        }
    }
}

看起来是不是一点问题都没有,然而不是。在实际运行中,总是存在部分漏网之鱼没有被去掉。

原因分析

下面我们对这个问题进行详细的分析。

先上代码:

public static void main(String[] args) {

    //构造测试数据
    List<Member> list = new ArrayList<>();
    list.add(new Member("zhangsan",17));
    list.add(new Member("lisi",16));
    list.add(new Member("wangwu",17));
    list.add(new Member("aaa",19));
    list.add(new Member("bbb",18));
    list.add(new Member("ccc",17));
    list.add(new Member("ddd",19));
    list.add(new Member("eee",20));
    list.add(new Member("fff",22));
    list.add(new Member("ggg",17));
    list.add(new Member("hhh",17));
    list.add(new Member("iii",17));

    removeMember(list);
    print(list);


}

/**
 * 打印Member List
 * @param list
 */
private static void print(List<Member> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
    System.out.println("-----------------------------");
}

/**
 * 删除年龄小于18岁的Member对象
 * @param list
 */
private static void removeMember(List<Member> list) {
    for (int i = 0; i < list.size(); i++) {
        if(list.get(i).getAge()<18){
            list.remove(i);
        }
    }
}

根据上述代码,应该得到的结果是所有大于等于18岁的member对象。

然而实际上输出结果是这样的:

Member{name='lisi', age=16}
Member{name='aaa', age=19}
Member{name='bbb', age=18}
Member{name='ddd', age=19}
Member{name='eee', age=20}
Member{name='fff', age=22}
Member{name='hhh', age=17}

lisihhh成了漏网之鱼,这放在生产环境中就成了大事故了。

原理解析

这里面涉及到一个list的操作问题,看过源码就知道是怎么回事了。

ArrayList实现类中的remove方法如下:

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

重点是这一句:System.arraycopy(elementData, index+1, elementData, index, numMoved);

ArrayList底层是数组来实现的,我们都知道(也许吧),数组是不支持在已有数组上进行扩容或缩容的,数组的扩容是通过数组的拷贝来实现的,也就是新创建一个更大长度的数组,将原来的数组内容拷贝到新数组中。

ArrayList中移除元素,就涉及到了数组的缩容。缩容也就只能将数组拷贝到一个新的数组中。就会涉及到数组中元素下标的移位。一张图可以展示上面代码的执行逻辑。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pRQtXw81-1648794262807)(F:\jskfb\技术文档库\开发技术\附件\image-20220401102439011.png)]

  • 解释一下,i=0时,对zhangsan这个家伙进行判断,发现age小于18,直接干掉。
  • 划重点!此时数组将变为i=1时对应的数组,zhangsan没了,这个list不能没有头啊,所以lisi赶紧补上去,后面的家伙们也赶紧往前冲,就形成了一个新的数组。但是,此时i=1了,已经指向了wangwu了。所以这一次的判断结束后,将会移除wangwu,得到的就是i=2时的数组。
  • 发现了吗?lisi就这么逃过了一劫。

结论分析

由此得出结论:

使用for循环遍历list来删除某个元素时,可能会存在漏网之鱼,也就是存在漏删的情况。(只删除一个元素是可行的)

其他方案分析

换一种方式,聊聊增强for循环。用增强for循环,会存在什么问题呢?

增强for循环删除元素

我们将removeMember方法改造一下

private static void removeMember(List<Member> list) {
        for (Member member : list) {
            if(member.getAge()<18){
                list.remove(member);
            }
        }
}

运行报错了。

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at Test1.removeMember(Test1.java:48)
	at Test1.main(Test1.java:26)

这是什么原因呢?还得看看源码:

ArrayList实现的remove(Object o)方法如下:

public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
}

看不出来问题,看看fastRemove(index)这个方法

 private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
}

也没啥毛病,那问题出在哪里呢?

跟一下异常堆栈信息,at java.util.ArrayList$Itr.next(ArrayList.java:859)

看来是在循环的时候出的问题。

跟进代码发现,这个next()方法是在list循环的时候调用的

public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

调用前先进行了校验修改

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

我们给这打个断点:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GBDsx0YT-1648794262808)(F:\jskfb\技术文档库\开发技术\附件\image-20220401110828682.png)]、

可以看到是这个地方modCountexpectedModCount值不一致导致抛出的异常。这事就麻烦大了。。。

再看看上面的next()方法,这个方法是一个Iterator迭代器的实现类来实现的。也就是说,增强for循环,实际上是迭代器的简化写法?

事实的确如此,在增强for循环中,集合遍历是通过iterator进行的。

但是元素的add/remove却是直接使用的集合类自己的方法(没有使用迭代器中的remove方法,迭代器中有单独的remove方法,后面再讲)。这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除/添加了,就会抛出一个异常,用来提示用户,可能发生了并发修改。当然,本次实际上倒是没有并发修改,只是循环和删除,是在两个不同的来源中处理的,也可以理解为并发修改了。

所以,增强for循环删除元素没问题,但是删除了还要继续遍历,那就不行了。

那差不多意思就是,在for循环和增强for循环中,删除一个元素是没问题的。删除多个元素,可能就会出大问题。

那到底要用什么样的方式来对循环对list中的元素进行操作呢?正确的姿势就是,使用迭代器Iterator

使用迭代器Iterator删除元素

我们再改造一下removeMember()方法

  private static void removeMember(List<Member> list) {
        Iterator<Member> iterator = list.iterator();
        while (iterator.hasNext()){
            Member next = iterator.next();
            if(next.getAge()<18){
                iterator.remove();
            }
        }
}

输出结果:

Member{name='aaa', age=19}
Member{name='bbb', age=18}
Member{name='ddd', age=19}
Member{name='eee', age=20}
Member{name='fff', age=22}

这一波很稳,没有任何问题。

实际上,在阿里巴巴java开发手册中就已经有规定了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-inLnkbI1-1648794262808)(F:\jskfb\技术文档库\开发技术\附件\image-20220401112410646.png)]

关于迭代器的实现,也是可以根据源码来分析的,下次咱可以再详细去分析分析。

结论

总之,通过这个案例,我们可以得出一个结论:

  • 尽量不要在for循环或增强for循环中取删除list中的元素。
  • 对元素的删除,统一使用Iterator迭代器。
  • 开发这件事,不要停留在粗略的表面,一定要尽量弄清楚原理,不然出现错误都找不到错在哪里!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云间歌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值