【Java基础】探索List和Map循环遍历删除问题

简介: Java代码写的其实不多,上周写List和Map的遍历,需要删除里面的元素时,直接就抛出异常,因为接触Java时间并不长,这种方式之前也很少使用,所以感觉这里肯定有坑,然后Java对List和Map的遍历方式也是五花八门,今天想花点时间研究了一下。

WCFF8$EG$R2PG[(4$]H)MC4.jpg

通过源码解读Java中List和Map循环遍历导致的删除问题。


前言


Java代码写的其实不多,上周写List和Map的遍历,需要删除里面的元素时,直接就抛出异常,因为接触Java时间并不长,这种方式之前也很少使用,所以感觉这里肯定有坑,然后Java对List和Map的遍历方式也是五花八门,今天想花点时间研究了一下。


问题引入


我们先看List的4种遍历情况,你看哪种会有问题:

List<String> platformList = new ArrayList<>();
platformList.add("掘金");
platformList.add("知乎");
platformList.add("微信公账号");
// List情况1:
for (String platform : platformList) {
    if (platform.equals("掘金")) {
        platformList.remove(platform);
    }
}
System.out.println(platformList);
// List情况2:
platformList.forEach(platform -> {
    if (platform.equals("知乎")) {
        platformList.remove(platform);
    }
});
System.out.println(platformList);
// List情况3:
Iterator iterator3 = platformList.iterator();
while (iterator3.hasNext()) {
    String platformStr = (String) iterator3.next();
    if (platformStr.equals("掘金")) {
        platformList.remove(platformStr);
    }
}
System.out.println(platformList);
// List情况4:
Iterator<String> iterator4 = platformList.iterator();
while (iterator4.hasNext()) {
    String platformStr = iterator4.next();
    if (platformStr.equals("掘金")) {
        iterator4.remove();
    }
}
System.out.println(platformList);

我想很多同学都知道,情况1、2、3都会抛出异常,情况4是没有问题的。

image.gif9U69J~SNF%AAW9IJMNHH]$O.png

如果我再多问一句,为什么会这样呢?


问题分析


异常使用

我们以“List情况1”为例,结合源码分析一下。其实“List情况1”和“List情况2”都是一样的,执行循环过程中,其实是使用的Iterator,使用的核心方法是hasnext()和next(),所以“List情况1”和“List情况2”,可以转换成“List情况3”的方式:

// List情况3:
Iterator iterator3 = platformList.iterator();
while (iterator3.hasNext()) {
    String platformStr = (String) iterator3.next();
    if (platformStr.equals("掘金")) {
        platformList.remove(platformStr);
    }
}
System.out.println(platformList);

下面我们看一下“List情况1”的执行过程:

5SSPDG1(VU[{6GS4HP5]QFF.png

6GUX[@P{XLDUVF0FJ8RO18L.png{C`_Z1C[GQF89J@2R[6{KVT.pngimage.gif

E9`4)P]TV}%J9SJJ`GAEV5V.png

218W`NJ$(ZIDQOIP}ICHI)O.png

VM%XMVSEX~T060B[N2LZB5Q.png

image.gifimage.gifimage.gif

整个流程非常清晰,其实就是因为List中维护了一个变量expectedModCount,该变量是List的初始大小,当你新增或者删除元素时,会修改临时变量modCount的值,每次循环时,Java会判断两个值是否相等,如果不相等,就会抛出异常。


正常使用

那为什么“List情况4“就没问题呢?我们看一下“List情况4“的执行过程:

第1步:

image.gif9JLIVF)PQ{G@C}(KEZ3)QID.png

第2步:

image.gifL5PQGBVEB]NHMBW}V4_HCGI.png

第3步:

3FW$}L7D9]%[{]N@G{5YCWC.png

第4步:WBA@F@$Z]D{DBD_D@~T`WAX.png

第5步:YKOXA3V3F~{2}(A3V9R5_~R.png

为了方便说明,我把步骤标明了一下,我们发现第1、2、4步执行的方法和前面“异常使用”的流程是一模一样的,但是第3步中有个remove()方法,这个其实是Iterator的remove()方法,然后第4步remove()方法其实是List的remove()方法,两个是嵌套关系。当执行完List的remove()方法后,Iterator的remove()方法会执行“第5步”的“expectedModCount = modCount”,重新让两者相等,这就是使用迭代器删除数据,不会抛出异常的原因所在。


扩展到Map


其实Map和List基本差不多,我们看看Map的使用情况:

Map<Integer, String> platformMap = new HashMap<>();
platformMap.put(1, "掘金");
platformMap.put(2, "知乎");
platformMap.put(3, "微信公账号");
// Map情况1:
for (Map.Entry<Integer, String> entry : platformMap.entrySet()) {
    Integer entryKey = entry.getKey();
    if (entryKey.equals(1)) {
        platformMap.remove(1);
    }
}
System.out.println(platformMap);
// Map情况2:
platformMap.forEach((key, value) -> {
    if (key.equals(1)) {
        platformMap.remove(1);
    }
});
System.out.println(platformMap);
// Map情况3:
Iterator<Integer> iterator = platformMap.keySet().iterator();
while (iterator.hasNext()) {
    Integer platformMapKey = iterator.next();
    if (platformMapKey.equals(1)) {
        iterator.remove();
    }
}
System.out.println(platformMap);

直接给出结论:“Map情况1”和“Map情况2”属于异常使用,“Map情况3”属于正常使用。

我们直接看看Map的迭代器执行remove()时的代码:

FTS]9G{1GOAEP1SUV){B}`K.png

看到没,迭代器的remove()同样有强制相等的代码,然后removeNode()方法属于Map的成员方法,和List的设计方式一模一样。


结语


文章简单探索了Java中List和Map循环遍历导致的删除问题,如果需要避免踩坑,可以使用迭代器,或者也可以通过for循环,然后采用指针位移的方式,这个就有点类似于C++的数组遍历中删除的方式。

然后网上也有说通过removeIf()方法来代替Iterator的remove(),这个大家可以试一下。

相关文章
go语言中遍历映射(map)
go语言中遍历映射(map)
423 8
|
存储 JavaScript 前端开发
for...of循环在遍历Set和Map时的注意事项有哪些?
for...of循环在遍历Set和Map时的注意事项有哪些?
773 156
|
安全 Java 数据库连接
让我们讲解一下 Map 集合遍历的方式
我是小假 期待与你的下一次相遇 ~
412 43
|
11月前
|
Java
Java编程:理解while循环的使用
总结而言, 使用 while 迴圈可以有效解决需要多次重复操作直至特定條件被触发才停止執行任务场景下问题; 它简单、灵活、易于实现各种逻辑控制需求但同时也要注意防止因邏各错误导致無限迁璇発生及及時處理可能発生异常以确保程序稳定运作。
646 0
|
存储 安全 Go
Map的遍历与判断键是否存在-《Go语言实战指南》
本文介绍了 Go 语言中对 `map` 的常见操作,包括遍历所有项和判断键是否存在。通过 `for range` 可以遍历 `map` 的键值对、仅键或仅值(需忽略键)。注意,`map` 遍历顺序是随机的。判断键是否存在时,使用双赋值语法 `value, ok := map[key]`,其中 `ok` 表示键是否存在。直接访问不存在的键会返回类型的零值,可能导致逻辑错误。掌握这些机制可更安全高效地处理键值对数据。
|
存储 Java 开发者
在 Java 中,如何遍历一个 Set 集合?
【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。
4819 113
|
人工智能 Java
Java 中数组Array和列表List的转换
本文介绍了数组与列表之间的相互转换方法,主要包括三部分:1)使用`Collections.addAll()`方法将数组转为列表,适用于引用类型,效率较高;2)通过`new ArrayList&lt;&gt;()`构造器结合`Arrays.asList()`实现类似功能;3)利用JDK8的`Stream`流式计算,支持基本数据类型数组的转换。此外,还详细讲解了列表转数组的方法,如借助`Stream`实现不同类型数组间的转换,并附带代码示例与执行结果,帮助读者深入理解两种数据结构的互转技巧。
1050 1
Java 中数组Array和列表List的转换
|
存储 监控 Java
《从头开始学java,一天一个知识点》之:数组入门:一维数组的定义与遍历
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问&quot;`a==b`和`equals()`的区别&quot;,大脑突然空白 - 写出的代码总是莫名报NPE,却不知道问题出在哪个运算符 这个系列就是为你打造的Java「速效救心丸」!我们承诺:每天1分钟,地铁通勤、午休间隙即可完成学习;直击痛点,只讲高频考点和实际开发中的「坑位」;拒绝臃肿,没有冗长概念堆砌,每篇都有可运行的代码标本。明日预告:《多维数组与常见操作》。 通过实例讲解数组的核心认知、趣味场景应用、企业级开发规范及优化技巧,帮助你快速掌握Java数组的精髓。
575 23
|
传感器 安全 Java
《从头开始学java,一天一个知识点》之:循环结构:for与while循环的使用场景
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问&quot;`a==b`和`equals()`的区别&quot;,大脑突然空白
463 22