Java 移除List中的元素,这玩意讲究!

list.add(“F”);

list.add(“C”);

list.add(“C”);

[C, A, B, C, F, C, C]

怎么移除掉list里面的某个元素呢 ?

list里面给我们提供了4个方法 :

先看 remove ( Object o) :

这个方面字面意思看,就是,你想移除list里面的哪个 Object ,你传进来就可以。

看源码,如下图:

也就是说并不是想移除哪个传哪个就能移除完, 而仅仅是只移除首个符合规则的元素。

结合例子:

现在这个List里面,存在4 个  “C” 元素 , 使用remove(“C”):

List list = new ArrayList();

list.add(“C”);

list.add(“A”);

list.add(“C”);

list.add(“B”);

list.add(“F”);

list.add(“C”);

list.add(“C”);

System.out.println(“未移除前”+list.toString());

list.remove(“C”);

System.out.println(“移除后”+list.toString());

结果:

未移除前[C, A, C, B, F, C, C]

移除后[A, C, B, F, C, C]

所以,光这样使用remove是不行的,不能实现我们需求 : 移除 list中的所有符合条件的元素,仅仅移除了符合条件的第一个 元素了。

这时候,大家可能就会想,那么我们就循环+remove呗,这样就能把每个符合条件的移除了。

真的吗?

接着看。

循环 +   remove ( Object o)/ remove(Index i):

没错,我们可以配合循环,把list里面的“C”元素都移除。

循环自然有分 while循环和 for循环(包含foreach) 。

先看  foreach方式  :

不得行! 切记!

for (String str: list){

if (“C”.equals(str)){

list.remove(str);

}

}

代码看似没问题,但是在foreach 使用 list的 remove / add 方法都是不行的!

报错:

ConcurrentModificationException  :  并发异常

PS: 其实如果大家曾阅读过阿里的开发规范,也许会有一点印象。

7.【强制】不要在foreach循环里进行元素的remove/add 操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。

那么先不管,如果你阅读过,可能也不一定知道里面的原理,所以继续往下看吧。

在分析这个错误前,我来提一嘴 ,一部分的ArrayList的特性:

ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长。

ArrayList不是线程安全的。

支持快速随机访问,通过下标序号index进行快速访问。

接下来,跟着我,一起来分析这个报错的出现 (当然我的挖错方式不一定适合大家,但是也可以参考):

1. 分析出错的代码段

for (String str: list){

if (“C”.equals(str)){

list.remove(str);

}

}

光这样看,我们只能知道,用了foreach的语法糖,那么我们看编译后的:

再看我们的报错信息:

源码分析:

通过我们反编译的代码,结合ArrayList的源码, 我们可以知道,

Itr 就是ArrayList里面的内部类,

而foreach的语法糖其实就是帮我们 new了一下 Itr,先调用hashNext()

while(var2.hasNext())

显然是作为循环的条件,那么我们也一起来简单看下这个方法源码:

public boolean hasNext() {

return cursor != size;

}

size是啥?

那cursor是啥?

所以,hashNext() 意思是, 当cursor 不等于 size的时候,代表 还有下一位,继续循环就完事了,这个值其实不是本次报错的重点。

我们继续看 Itr的next()方法中的 checkForComodification()方法,就是这玩意导致报错的。

那么我们直接定位到  checkForComodification()方法的源码:

代码简单, 也看到了我们刚才报的错 ConcurrentModificationException 在里面躺着。

只要modCount 不等于 expectedModCount ,就抛错。

那么我们就得明白  modCount 和 expectedModCount是啥?

expectedModCount简单,是Itr里的一个属性 ,在初始化的时候,就已经把 modCount的值 等赋给了 expectedModCount。

其实 expectedModCount 就是用来记录 一开始 迭代的 list的 变更数modCount, 至于 list的 变更数modCount是啥,我们接着看。

点进去看modCount的源码:

可以看到作者真是苦口婆心,这么一个字段属性,人家写了这么多注释, 那肯定是解释得非常细致了。

那么我来抽出一些核心的 翻译一下,给各位看看:

此列表在结构上被修改的次数。结构修改是指改变结构尺寸的修改。

如果此字段的值意外更改,则迭代器(或列表迭代器)将在

对{@code next}、{@code remove}、{@code previous}的响应,{@code set}或{@code add}操作。这提供了快速失败行为,而不是迭代过程中并发修改的情况。

我来简单再说一下:

这个modCount,可以理解为记录list的变动值。 如果你的list里面连续add 7个元素,那么这个变动值就是7 . 如果是add 7个元素,remove 1个元素, 那么这个值就是8 . 反正就是修改变动的次数的一个统计值。

而这个值,在使用迭代的时候,会在迭代器初始化传入,赋值给到迭代器 Itr 里面的内部记录值 ,也就是我们刚刚讲到的 expectedModCount 值 。 这样来防止使用的时候,有意外的修改,导致并发的问题。

这么一说,其实我们报错ConcurrentModificationException  的原因就很明显了。

一开始的情况:

所以在我们第一次循环检测,使用foreach语法糖,调用  Itr的next()方法时,会去调用 check方法:

因为确实一开始大家都是7,检测modCount和 expectedModCount值是通过的:

接着,我们继续触发  Itr的next()方法,按照往常,也是调用了check方法,结果检测出来初始化传入的list变化记录数expectedModCount是7,而 最新的list的变更记录数modCount 因为在第一次的list.remove触发后,modCount++了,变成了8,所以:

两值不等, 抛出错误。

所以上述出现报错 ConcurrentModificationException 的原因非常明了, 其实就是因为调用了 Itr的next()方法, 而next()方法每次执行时,会调check方法。 那么可以理解为,这是foreach语法糖+移除时的锅。

那么我们就避免这个语法糖 ,我们先来个习惯性编写的for循环方式:

List list = new ArrayList();

list.add(“C”);

list.add(“A”);

list.add(“C”);

list.add(“B”);

list.add(“F”);

list.add(“C”);

list.add(“C”);

System.out.println(“未移除前” + list.toString());

int size = list.size();

for (int i = 0; i < size; i++) {

if (“C”.equals(list.get(i))){

list.remove(“C”);

}

}

System.out.println(“移除后” + list.toString());

这样的执行结果是啥, 报错了,IndexOutOfBoundsException 数组索引边界异常:

为啥会错啊,原因很简单:

ps: cv习惯了,蓝色字体里已经cv不分了,也不改了,大家意会即可。

所以这个示例报错的原由很简单,我编码问题,把size值提前固定为7了, 然后list的size是实时变化的。

那么我把size不提前获取了,放在for循环里面。这样就不会导致 i++使 i大于list的size了:

List list = new ArrayList();

list.add(“C”);

list.add(“A”);

list.add(“C”);

list.add(“B”);

list.add(“F”);

list.add(“C”);

list.add(“C”);

System.out.println(“未移除前” + list.toString());

for (int i = 0; i < list.size(); i++) {

if (“C”.equals(list.get(i))) {

list.remove(“C”);

}

}

System.out.println(“移除后” + list.toString());

}

这样的运行结果是什么:

虽然没报错,但是没有移除干净,为什么?

其实还是因为 list的size在真实的变动 。每次移除,会让size的值 -1 , 而 i 是一如既往的 +1 .

而因为ArrayList是数组, 索引是连续的,每次移除,数组的索引值都会 ’重新编排‘ 一次。

看个图,我画个简单的例子给大家看看:

也就是说,其实每一次的remove变动, 因为我们的循环 i值是一直 增加的,

所以会造成,我们想象的  数组内第二个 C 元素 的索引是 2, 当i为2时会 拿出来检测,这个假想是不对的。

因为如果 第二个 C 元素前面的 元素发生了变化, 那么它自己的索引也会往前 移动。

所以为什么会出现 移除不干净的 现象    **,

其实简单说就是    最后一个C元素因为前面的元素变动移除/新增,它的 index变化了。

然后i > list.size() 的时候就会 跳出循环, 而这个倒霉蛋 C元素排在后面,index值在努力往前移,而 i 值在变大, 但是因为我们这边是执行remove操作, list的size 在变小。**

在 i值和 size值 两个 交锋相对的时候,最后一个C元素没来得及匹对, i就已经大于 list.size ,导致循环结束了。

这么说大家不知道能不能懂,因为对于初学者来说,可能没那么快可以反应过来。

没懂的兄弟,看我的文章,我决不会让你带着疑惑离开这篇文章,我再上个栗子,细说(已经理解的可以直接往下拉,跳过这段罗嗦的分析)。

上栗子:

我们的list 里面 紧紧有 三个元素    “A”  “C”  “C” , 然后其余的不变,也是循环里面移除”C“ 元素 。

List list = new ArrayList();

list.add(“A”);

list.add(“C”);

list.add(“C”);

System.out.println(“未移除前” + list.toString());

for (int i = 0; i < list.size(); i++) {

if (“C”.equals(list.get(i))) {

list.remove(“C”);

}

}

System.out.println(“移除后” + list.toString());

先看一下结果,还是出现移除不干净:

分析:

1. list的样子:

2. 循环触发,第一次 i 的值为 0, 取出来的 元素是 A ,不符合要求:

3.继续循环, 此时list的size值 依然是不变,还是 3 ,而i的值因为 i++ 后变成了1 , 1 小于 3,条件符合,进入循环内,取出 list里 index为 1的元素:

4.这个 C符合要求, 被移除, 移除后,我们的 list状态变成了:

5. 此时此刻 list的 size 是 2 ,再一轮for循环 ,  i 的值 i++ 后继续变大,从1 变成了 2 ,  2不小于 2 ,所以循环结束了。

但是我们这时候list里面排在最后的那个C元素 原本index是 2,变成了index 1 ,这个家伙 都还没被 取出来, 循环结束了,它就逃过了检测。 所以没被移除干净。

PS: 很可能有些看客 心里面会想(我YY你们会这么想), 平时用的remove是利用index移除的, 跟我上面使用的 remove(Object o) 还不一样的,是不是我例子的代码使用方法问题。

然而并不然,因为这个remove调用的是哪个,其实不是重点,看图:

结果还是一样:

其实 这样的for循环写法, 跟  list的remove 到底使用的是 Object匹配移除 还是 Index移除 , 没有关系的。 移除不干净是因为 循环 i的值 跟 list的size变动 ,跳出循环造成的。

能看到这里的兄弟, 辛苦了。

那么 使用 remove 这个方法,结合循环,那就真的没办法 移除干净了吗?

行得通的例子:

while循环 :

List list = new ArrayList();

list.add(“C”);

list.add(“A”);

list.add(“C”);

list.add(“B”);

list.add(“F”);

list.add(“C”);

list.add(“C”);

System.out.println(“未移除前”+list.toString());

while (list.remove(“C”));

System.out.println(“移除后”+list.toString());

}

结果,完美执行:

为什么这么些 不会报ConcurrentModificationException错,也不会报 IndexOutOfBoundsException 错 呢?

我们看看编译后的代码:

可以看到时单纯的调用list的remove方法而已,只要list里面有"C",那么移除返回的就是true,那么就会继续触发再一次的remove(“C”),所以这样下去,会把list里面的“C”都移除干净,简单看一眼源码:

所以这样使用是行得通的。

那么当然还有文章开头我给那位兄弟说的使用迭代器的方式动态删除也是行得通的:

Iterator

List list = new ArrayList();

list.add(“C”);

list.add(“A”);

list.add(“B”);

list.add(“C”);

list.add(“F”);

list.add(“C”);

list.add(“C”);

System.out.println(“未移除前” + list.toString());

Iterator it = list.iterator();

while(it.hasNext()){

String x = it.next();

if(“C”.equals(x)){

it.remove();

}

}

System.out.println(“移除后” + list.toString());

执行结果:

PS:

但是这个方式要注意的是, if判断里面的顺序,

一定要注意把 已知条件值前置 :  “C”.equals ( xxx) , 否则当我们的list内包含null 元素时, null是无法调用equals方法的,会抛出空指针异常。

那么其实我们如果真的想移除list里面的某个 元素,移除干净 。

我们其实 用removeAll ,就挺合适。

removeAll

list.removeAll(Collections.singleton(“C”));

或者

list.removeAll(Arrays.asList(“C”));

List list = new ArrayList();

list.add(“C”);

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

那么其实我们如果真的想移除list里面的某个 元素,移除干净 。

我们其实 用removeAll ,就挺合适。

removeAll

list.removeAll(Collections.singleton(“C”));

或者

list.removeAll(Arrays.asList(“C”));

List list = new ArrayList();

list.add(“C”);

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-LHDKPjca-1715346961444)]

[外链图片转存中…(img-XCXo7TUI-1715346961444)]

[外链图片转存中…(img-az9gw6vk-1715346961445)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值