百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

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

toyiye 2024-07-15 01:22 6 浏览 0 评论

起因

最近接到一个需求(假的),从一个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中移除元素,就涉及到了数组的缩容。缩容也就只能将数组拷贝到一个新的数组中。就会涉及到数组中元素下标的移位。一张图可以展示上面代码的执行逻辑。

解释一下,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();
}

我们给这打个断点:

可以看到是这个地方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开发手册中就已经有规定了。

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

结论

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

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



相关推荐

Python 可视化工具包(python常见的可视化工具)

喜欢用Python做项目的小伙伴不免会遇到这种情况:做图表时,用哪种好看又实用的可视化工具包呢?本文将介绍一些常用的Python可视化包,包括这些包的优缺点以及分别适用于什么样的场景。这篇文章...

Python的GPU编程实例——近邻表计算

目录技术背景...

python算法体验-3.python实现欧式距离的三种方式

欧式距离也称欧几里得距离,是最常见的距离度量,衡量的是多维空间中两个点之间的绝对距离。欧式距离源自N维欧氏空间中两点...

python实现Lasso回归分析(特征筛选、建模预测)

实现功能:...

python语言检测模块langid、langdetect使用

本文首发地址:https://blog.csdn.net/Together_CZ/article/details/86678423欢迎关注我的博客【Together_CZ】,我是沂水寒城!之前使用数据...

7天学会Python最佳可视化工具Seaborn(一):可视化变量间的关系

众所周知,Seaborn“可能”是Python下最友好、易用的可视化工具了,可视化效果也非常好。但是截止目前,并没有一份中文教程供广大国内Python使用者查阅学习。怎么能因为语言的问题,让大家错过这...

在Python中使用K-Means聚类和PCA主成分分析进行图像压缩

各位读者好,在这篇文章中我们尝试使用sklearn库比较k-means聚类算法和主成分分析(PCA)在图像压缩上的实现和结果。压缩图像的效果通过占用的减少比例以及和原始图像的差异大小来评估。图像压...

OpenCV-Python 相机校准 | 四十九

目标在本节中,我们将学习由相机引起的失真类型,如何找到相机的固有和非固有特性如何根据这些特性使图像不失真基础一些针孔相机会给图像带来明显的失真。两种主要的变形是径向变形和切向变形。径向变形会导致直线出...

python数据预处理技术(python 数据预处理)

在真实世界中,经常需要处理大量的原始数据,这些原始数据是机器学习算法无法理解的。为了让机器学习算法理解原始数据,需要对数据进行预处理。我们运行anaconda集成环境下的“jupyternotebo...

【Python可视化系列】一文教你绘制不同类型散点图(理论+源码)

这是...

OpenCV-Python 特征匹配 | 四十四

目标在本章中,我们将看到如何将一个图像中的特征与其他图像进行匹配。我们将在OpenCV中使用Brute-Force匹配器和FLANN匹配器Brute-Force匹配器的基础蛮力匹配器很简单。它使用第一...

实战python中Random模块使用(python中的random模块)

一、random模块简介Python标准库中的random函数,可以生成随机浮点数、整数、字符串,甚至帮助你随机选择列表序列中的一个元素,打乱一组数据等。要在Python中使用random模块,只需要...

Python随机模块22个函数详解(python随机函数的应用)

随机数可以用于数学,游戏,安全等领域中,还经常被嵌入到算法中,用以提高算法效率,并提高程序的安全性。平时数据分析各种分布的数据构造也会用到。random模块,用于生成伪随机数,之所以称之为伪随机数,是...

说冲A就冲A,这个宝藏男孩冯俊杰我pick了

爱奇艺新上架了一部网剧叫《最后一个女神》。有个惊人的发现,剧里男三居然是《青春有你》的训练生冯俊杰。剧组穷,戏服没几件,冯俊杰几乎靠一件背背佳撑起了整部剧。冯俊杰快速了解一下。四川人,来自觉醒东方,人...

唐山打人嫌犯陈继志去医院就医的背后,隐藏着三个精心设计的步骤

种种迹象表明,陈继志这帮人对处理打人之后的善后工作是轻车驾熟的,他们想实施的计划应该是这样的:首先第一步与伤者进同一家医院做伤情鉴定,鉴定级别最好要比对方严重,于是两位女伤者被鉴定为轻伤,他们就要求医...

取消回复欢迎 发表评论:

请填写验证码