相关阅读

【小家java】java5新特性(简述十大新特性) 重要一跃
【小家java】java6新特性(简述十大新特性) 鸡肋升级
【小家java】java7新特性(简述八大新特性) 不温不火
【小家java】java8新特性(简述十大新特性) 饱受赞誉
【小家java】java9新特性(简述十大新特性) 褒贬不一
【小家java】java10新特性(简述十大新特性) 小步迭代
【小家java】java11新特性(简述八大新特性) 首个重磅LTS版本


每篇一句

一个男人最无能为力的就是,在他最没能力的年纪,遇见了他最想照顾一辈子的姑娘

1、概述

java中的集合框架是我们日常使用得最多的数据结构,而List作为Collection里最重要的一员,使用就更加的频繁了。因此我们平时使用中少不了对List的增删改查,本文就针对于对List的“删”操作进行一个分析,顺便说几个坑,希望能帮助到大家以后可以避免踩坑

2、栗子

有一个List,如果我们要删除其中的一个元素,怎么办呢? 这里我们先用remove方法

 public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("aa");list.add("bb");list.add("cc");System.out.println(list); //[aa, bb, cc]list.remove("aa");System.out.println(list); //[bb, cc]}

这个可能是我们使用得比较多的方式,直接remove即可。但,看下面例子,你能猜对结果吗?

public static void main(String[] args) {List<Integer> list = new ArrayList<>();list.add(2);list.add(1);list.add(0);System.out.println(list); //[2, 1, 0]list.remove(2);System.out.println(list); //[2, 1]}

惊喜不?我们发现 我们得到的答案:发现2并没有被删除掉,而是把index为2的0元素的删除掉了。这是什么呢?原来,list里面有两个重载方法:

E remove(int index); //返回删除的元素
boolean remove(Object o); //返回bool值

显然,如果删除的元素不是整数,那是没有异议的。那如果删除的元素恰好就是整数呢?(如上面的例子),我们先来看看官方怎么说:

囧,查看了java官方文档,并没有找到相关的解释。(如果你有找到,请直接mark我哈)

不管了,看代码的执行效果是不会骗人的,如果你直接传数字,表示的是index,所以注意index一定不能out越界了哟。那么问题来了,如上例子,如果就想强制调用boolean remove(Object o)方法怎么办呢?官方虽然没有解释,但此处小编给一个答案给各位同学参考哈:

  public static void main(String[] args) {List<Integer> list = new ArrayList<>();list.add(2);list.add(1);list.add(0);System.out.println(list); //[2, 1, 0]Integer ele = 2;list.remove(ele);System.out.println(list); //[1, 0]}

我们会发现这样子,就是根据元素来删除的。或者这样子简写也是ok的

 public static void main(String[] args) {List<Integer> list = new ArrayList<>();list.add(2);list.add(1);list.add(0);System.out.println(list); //[2, 1, 0]list.remove((Integer)2);System.out.println(list); //[1, 0]}

我们发现,两个重载方法都是匹配的,那怎么就走那个了呢?因此为了给大家解惑,我这里简单介绍一下java的重载算法(或者说是匹配优先级):

  1. 先匹配参数个数
  2. 参数类型的最佳匹配:直接所属类(注意此处说的是最佳匹配)
  3. 如果没有找到直接所属类,会发生向上转型,直至找父类参数,直观上查找顺序为:包装类-》父类-》接口
  4. 如果向上转型仍无法匹配,则查找可变参数列表
  5. 以上无法匹配返回找不到方法错误(其实编译就会报错了)

显然我们发现,我们两个remove方法会在第三条匹配成功(注意:只会向上转型,而不会向下拆箱哟)。

天空一声巨响,主菜登场。。。

其实我们要删除List里面元素的时候,大多数情况下是进行循环删除。但上面两个删除是基础,因此下面介绍一下List循环删除的相关case,例如我现在有如下一个集合:

List<Integer> list = new ArrayList<>();list.add(2);list.add(1);list.add(3);list.add(5);list.add(8);list.add(6);list.add(2);list.add(5);list.add(10);

备注:这里插一嘴,因为咱们要对list进行remove操作,所以此处对list的初始化,一定不能这么做:

List<Integer> list = Arrays.asList(2, 1, 3, 5, 8, 6, 2, 5, 10);

因为这么生成的list其实是Arrays自己实现的一个阉割版的List,它是木有实现remove方法的,所以无法实现删除操作。这里附上部分源码供参考:

//1、普通for循环遍历
Integer baseNum = 9; //以这个为基础 删除掉>=这个值的元素
System.out.println("剩余长度:" + list.size() + "---" + list);
for (int i = 0; i < list.size(); i++) {if (list.get(i) >= baseNum)list.remove(i);
}
System.out.println("剩余长度:" + list.size() + "---" + list);
输出结果如下:
剩余长度:9---[2, 1, 3, 5, 8, 6, 2, 5, 9]
剩余长度:8---[2, 1, 3, 5, 8, 6, 2, 5]

表面上看,好像达到了我们的效果,是不是以后我们以后就可以用这种方法来删除了呢?如果你觉得ok,那就too young too simple AND sometimes native。咱们把baseNum调整成5再试一遍:

//1、普通for循环遍历
Integer baseNum = 5; //以这个为基础 删除掉>=这个值的元素
System.out.println("剩余长度:" + list.size() + "---" + list);
for (int i = 0; i < list.size(); i++) {if (list.get(i) >= baseNum)list.remove(i);
}
System.out.println("剩余长度:" + list.size() + "---" + list);
输出结果如下:
剩余长度:9---[2, 1, 3, 5, 8, 6, 2, 5, 9]
剩余长度:6---[2, 1, 3, 8, 2, 9]

我们发现8和9元素竟然都还在没有被删除。所以,这种删除方式肯定是有问题的:问题在于,删除某个元素后,list的大小发生了变化,而你的索引也在变化,所以会导致你在遍历的时候漏掉某些元素(如上,8个9就被漏掉了)。按照我们的想法,我们用增强for循环?

//2、增强for循环
Integer baseNum = 5; //以这个为基础 删除掉>=这个值的元素System.out.println("剩余长度:" + list.size() + "---" + list);for (Integer x : list) {if (x >= baseNum)list.remove(x); //java.util.ConcurrentModificationException}System.out.println("剩余长度:" + list.size() + "---" + list);

这种方式的问题在于,删除元素后继续循环会报错误信息ConcurrentModificationException,因为元素在使用的时候发生了并发的修改,导致异常抛出(其实这是很重要的fast-failed机制,后面博文会专门讨论这种机制的优点)。但是如果我们只需要删除一个元素,马上使用break跳出,则不会触发报错。
要了解这个为什么报错,我们需要知道两点:
1、增强for循环到额原理是什么?
为了给大家解释清楚这个问题,我特意找到了.class文件,让大家看看增强for的真身:

看到编译后的代码,我们发现底层还是有迭代器实现的,并且,并且,并且,你会发现它调用的是list的remove方法,但是这却不是报错的根源,咱们得继续看下面的源码分析
2、fast-failed机制什么时候会触发?
从报错信息中看,remote方法没有报错,报错的是next方法,因此我贴出源码

这里面有快速失败的检查,所以最终结论是,我们也不能用增强for删除。那怎么办呢?

最后介绍一种正确的方法(推荐):

//3、迭代器iterator遍历
Integer baseNum = 5; //以这个为基础 删除掉>=这个值的元素
System.out.println("剩余长度:" + list.size() + "---" + list);
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {Integer x = it.next();if (x >= baseNum) {it.remove();//list.remove(x); //java.util.ConcurrentModificationException}
}
System.out.println("剩余长度:" + list.size() + "---" + list);
输出结果为:
剩余长度:9---[2, 1, 3, 5, 8, 6, 2, 5, 9]
剩余长度:4---[2, 1, 3, 2]

bingo,终于拿到了我们想要的结果了。但此处一定要注意,一定要注意,一定要注意。重说三,我们remove的时候,一定只能使用迭代器的remove方法,否则也还是会报错的,重点一定要注意。

最后,我再介绍两种也能同样达到效果的删除方法,但是并不推荐这么去做,知道就行了
方法一:使用CopyOnWriteArrayList 支持并发修改的List来代替,缺点就是效率低
方法二:采用对List倒序遍历的方式。缺点是:理解难道相较于迭代器偏大,需要结合List内部实现才能真正理解。当然因为size需要实时计算,速度上也不占优势

 Integer baseNum = 5;System.out.println("剩余长度:" + list.size() + "---" + list);for (int i = list.size() - 1; i >= 0; i--) {//for (int i = 0; i < list.size(); i++) {if (list.get(i) >= baseNum)list.remove(i);}System.out.println("剩余长度:" + list.size() + "---" + list);

这里再说说关于Map的删除情况,怎么删除才能不抱错呢?

这样子,采用map的remove方法,也报错:

  public static void main(String[] args) {Map<Integer, Integer> map = new HashMap<>();map.put(1, 1);map.put(2, 2);map.put(5, 5);map.put(3, 3);map.put(10, 10);Iterator<Integer> iterator = map.keySet().iterator();while (iterator.hasNext()) {Integer key = iterator.next();if (key >= 5) {map.remove(key);}}System.out.println(map);}
报错:Exception in thread "main" java.util.ConcurrentModificationException

由此课件,map也有类似情况,因此此处举一例,map可以安全删除的例子

代码同上,只需要调用iterator.remove();即可,不要用map.remove(key);

3、使用场景

一句话:集合的使用场景有哪些,这个就有哪些。所以显然:everywhere!

小彩蛋

Map中的putIfAbsent、computeIfAbsent、computeIfPresent、compute

put方法参考:

    public static void main(String[] args) {Map<String, Integer> map = new HashMap<>();System.out.println(map.put("1", 2)); //null 如果是添加成功,返回nullSystem.out.println(map.put("1", 3)); //2  如果已经存在此key了 会用新值覆盖旧值  然后把oldValue返回出来System.out.println(map); //{1=3}}
putIfAbsent

如果给定的key不存在(或者key对应的value为null),就把value放进去,并返回null;
如果存在,返回当前值(不会把value放进去);

    public static void main(String[] args) {Map<String, Integer> map = new HashMap<>();System.out.println(map.putIfAbsent("1", 2)); //null 如果是添加成功,返回nullSystem.out.println(map.putIfAbsent("1", 3)); //2  如果已经存在此key了,不会覆盖此value值。  返回的还是oldValue System.out.println(map); //{1=2}}
computeIfAbsent(重要)

如果给定的key不存在(或者key对应的value为null),就去计算mappingFunction的值(计算结果决定了返回值有两种情况如下,我们姑且叫计算的结果为newValue):

若key已经存在,就不会执行mappingFunction。返回oldValue

  1. newValue == null,不会替换旧值。返回null
  2. newValue != null,替换旧值。返回新值
    public static void main(String[] args) {Map<String, Integer> map = new HashMap<>();System.out.println(map.computeIfAbsent("1", key -> Integer.valueOf(key) + 2)); //3 计算出来的newValue放进去,并且返回newValueSystem.out.println(map.computeIfAbsent("1", key -> Integer.valueOf(key) + 3)); //3 key已经存在了,不作为System.out.println(map.computeIfAbsent("2", key -> null)); //3 key虽然不存在,但计算出为null,所以不作为,并且返回nullSystem.out.println(map); //{1=3}}
computeIfPresent

和上面相反,当key存在且对应的oldValue不为null时,执行计算,否则啥都不做。

    public static void main(String[] args) {Map<String, Integer> map = new HashMap<>();System.out.println(map.computeIfPresent("1", (k, v) -> Integer.valueOf(k) + v)); //null key不存在,不作为  且返回nullSystem.out.println(map.computeIfPresent("1", (k, v) -> Integer.valueOf(k) + v)); //null  key还是不存在,不作为 且返回nullmap.put("1", 1);System.out.println(map.computeIfPresent("1", (k, v) -> Integer.valueOf(k) + v)); //2  key存在,开始计算。把计算的结果覆盖掉,然后返回计算的结果System.out.println(map); //{1=2}System.out.println(map.computeIfPresent("1", (k, v) -> null)); //null key存在,开始计算 但是计算结果为null。所以返回null,此处注意remove()掉了对应的keySystem.out.println(map); //{}}
典型应用场景(以computeIfAbsent为例)
    public static void main(String[] args) {Map<String, List<String>> map = new HashMap<>();List<String> list;// 以前一般这样写,各种判断==========================================================list = map.get("list-1");if (list == null) {list = new LinkedList<>();map.put("list-1", list);}list.add("one");//==========================================================// 使用 computeIfAbsent 可以这样写 这样若已存在,返回值oldValue,若不存在就执行计算,也是返回一个可用的List // 这样对null非常友好,特别好用~~~~~~~~~~list = map.computeIfAbsent("list-1", k -> new ArrayList<>());list.add("one");}

4、最后

集合作为我们使用最为广泛的数据结构,因此了解这里面的一些原理和正确做法,能够使得我们少采坑,希望此文能够帮助到你


关注A哥

Author A哥(YourBatman)
个人站点 www.yourbatman.cn
E-mail yourbatman@qq.com
微 信 fsx641385712
活跃平台
公众号 BAT的乌托邦(ID:BAT-utopia)
知识星球 BAT的乌托邦
每日文章推荐 每日文章推荐

【小家java】Java中集合List、Set、Map删除元素的方法大总结(避免ConcurrentModificationException异常)相关推荐

  1. java util 中set,List 和Map的使用

    Set对每个对象只接受一次,并使用自己内部的排序方法(通常,你只关心某个元素是否属于Set,而不关心它的顺序--否则应该使用List).Map同样对每个元素保存一份,但这是基于"键" ...

  2. C++中STL容器利用迭代器删除元素小结

    C++中STL容器利用迭代器删除元素小结 在STL容器中删除元素,容易导致迭代器失效,故应该清楚明白其用法,现在总结一下. 转载自:https://blog.csdn.net/yf_li123/art ...

  3. java程序中集合框架有哪些

    作者:代码界的小白 链接:Java中集合在面试中的高频考点_技术交流_牛客网 来源:牛客网 本章节主要分享一些Java中的集合在面试中常问的高频问题,这里给出的是相对比较简略的答案,不过针对面试的回答 ...

  4. JAVA基础-06.集合-15.【Map】

    01_Map集合遍历键找值方式 02_Map集合遍历键值对方式 03_练习_计算一个字符串中每个字符出现次数 04_斗地主案例需求分析 day04 [Map] 主要内容 Map集合 教学目标 能够说出 ...

  5. java重载中this的作用_Java2:构造方法、方法重载和this关键字

    上一篇博客小编已经给读者介绍了Java入门的类与对象,这一章节我们来了解一下构造方法,方法重载以及this关键字. 一:构造方法 Java中方法一般有2中:普通方法和构造方法.我们已经知道普通方法的声 ...

  6. 在基于图论的Java程序中基于DSL的输入图数据的方法

    我们大多数人已经编写了一些程序来处理图论算法,例如查找两个顶点之间的最短路径,查找给定图的最小生成树等等. 在这些算法的每一种中,表示图形的编程方式是使用邻接矩阵或邻接表 . 两者都不是定义图形输入的 ...

  7. JAVA 代码交互率低的原因分析,深入剖析Java编程中的中文问题及建议最优解决方法...

    说明:本文为作者原创,作者联系地址为: josserchai@yahoo.com .由于 Java 编程中的中文 问题是一个老生常谈的问题,在阅读了许多关于 Java 中文问题解决方法之后,结合作者的 ...

  8. java数组包含某个元素_java中判断数组是否包含某元素的方法

    有两种方法可以判断数组是否包含元素: 方法1, 将数组转换为list,然后使用list的contains方法来判断:Arrays.asList(...).contains(...) java.lang ...

  9. List集合遍历时如何删除元素

    背景:在业务中遍历某个集合时涉及到排除某个属性为0的情况,即遍历时删除. 一.3种错误方式演示三种: 1.foreach List<String> list = new ArrayList ...

最新文章

  1. k8s php mysql_在k8s上部署第一个php应用
  2. 使用CNN进行情感分类
  3. 报名通道开启 | 顶会 ICLR 2021:医疗对话生成与自动诊断国际竞赛,邀你来战!...
  4. tcp/ip,http,socket mysql底层技术原理
  5. gis计算机信息技术,信息技术(IT)对GIS的影响
  6. 比特币 POW 白皮书 区块篡改成功的概率 代码 注释详解
  7. 视频,多媒体本地化总结
  8. STM32F4应用笔记(二)利用蜂鸣器播放天空之城
  9. 大规模行人检索—PRCV2020竞赛发布
  10. iOS下86版五笔输入法练习程序(作为备忘,最基本功能的演示demo,版本:0.99版)
  11. erlang底层c定时器设计-Erlang源码学习二
  12. 移除未排序链表中的重复节点,保留最开始出现的节点
  13. C语言算法题:简单密码
  14. 使用命令行登陆Mysql
  15. “牛气冲天”预交卷,2022年雨花区会怎样“如虎添翼”?
  16. DiscuzX3.1数据库字典(含之前)
  17. surface pro linux服务器,Surface Pro平板电脑上安装Linux / Ubuntu的技巧
  18. cmdlet是什么意思?
  19. 深度学习训练技巧总结
  20. WIN10引用的账户当前已锁定,且可能无法登录的解决方案

热门文章

  1. mysql查询不以字母结尾,MySQL整数比较会忽略结尾的字母字符
  2. 小米10Pro手机双击android,小米10pro使用4天后的真实评测
  3. wampserver:Could not execute menu item.
  4. Ubuntu输入法问题解决方案
  5. shell脚本中的一些符号
  6. NC6 二叉树中的最大路径和
  7. “魔兽世界”的虚拟历险与现实世界的悲喜人生(摘自English Digest,个人整理排版)
  8. 保持计算机软件和硬件在预算之内的程序员指南
  9. GPT系列:生成式预训练与零样本学习
  10. 打开Activity自动显示手机输入法