当前位置: 首页 > article >正文

Java在用增强for循环遍历集合时删除元素,抛出java.util.ConcurrentModificationException异常

文章目录

  • 0. 前言
  • 1. 问题产生的背景
  • 2. Java中增强for循环的底层原理
  • 3. 为什么增强for循环不支持在遍历集合时删除元素
    • 3.1 问题排查
    • 3.2 modCount 变量的来源
    • 3.3 expectedModCount 变量的来源
    • 3.4 导致modCount变量和expectedModCount不相等的原因
    • 3.5 为什么用迭代器遍历元素时删除元素不会报错
    • 3.6 遍历 Map 等集合时删除元素会抛出 ConcurrentModificationException吗
  • 4. 如何正确地在遍历集合时删除元素
    • 4.1 使用迭代器进行删除(推荐使用)
    • 4.2 使用removeIf方法(推荐使用)
      • 4.2.1 实现了Collection接口的集合
      • 4.2.2 实现了Map接口的集合
    • 4.3 收集要删除的元素,遍历结束之后再删除
  • 5. 扩展:为什么用下标遍历元素集合时删除元素不会报错

0. 前言

本文讨论的是用增强 for 循环在遍历集合时删除元素的情况,用增强 for 循环在遍历集合时添加元素的情况类似

1. 问题产生的背景

在日常开发中,我们可能会遇到这样的需求,遍历一个集合,如果某些元素符合特定条件,我们就删除这些元素

我们以存储整数的 List 为例,如果是偶数,我们就从 List 中删除

在这里插入图片描述

import java.util.ArrayList;
import java.util.List;

public class ListDeleteTests {

    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            arrayList.add(i);
        }

        for (Integer i : arrayList) {
            if (i % 2 == 0) {
                arrayList.remove(i);
            }
        }

        arrayList.forEach(System.out::println);
    }

}

不出意外,运行代码后程序会抛出以下异常

在这里插入图片描述

Exception in thread “main” java.util.ConcurrentModificationException
at java.base/java.util.ArrayList I t r . c h e c k F o r C o m o d i f i c a t i o n ( A r r a y L i s t . j a v a : 1013 ) a t j a v a . b a s e / j a v a . u t i l . A r r a y L i s t Itr.checkForComodification(ArrayList.java:1013) at java.base/java.util.ArrayList Itr.checkForComodification(ArrayList.java:1013)atjava.base/java.util.ArrayListItr.next(ArrayList.java:967)
at cn.edu.scau.ListTests.main(ListTests.java:15)


为什么会抛出这个异常呢,我们一步一步分析

2. Java中增强for循环的底层原理

增强 for 循环的底层原理基于迭代器(Iterator),以下是增强 for 循环的工作原理:

  1. 目标集合:增强for循环可以用于任何实现了Iterable接口的对象,这意味着它可以直接用于大多数集合类型,如ListSetQueue(增强for循环通过自动装箱和自动拆箱来处理基本数据类型)
  2. 迭代器(Iterator):当增强for循环开始执行时,它会获取目标集合的迭代器。这个迭代器负责跟踪遍历的当前位置,并提供对集合中每个元素的访问
  3. 循环体:在每次迭代中,增强for循环调用迭代器的next()方法来获取下一个元素,并将其赋值给循环变量。然后执行循环体中的代码
  4. 结束条件:增强for循环会继续执行,直到迭代器的hasNext()方法返回false,这表示没有更多元素可以遍历

增强 for 循环的一些关键点:

  • 简洁性:增强 for 循环提供了更简洁的语法,不需要显式地创建迭代器
  • 限制:增强 for 循环不支持在遍历时修改集合(添加、删除元素),因为这样做可能会导致ConcurrentModificationException
  • 只读访问:增强for循环通常用于只读操作,如果要进行修改操作,通常需要使用传统的for循环或迭代器

总的来说,增强 for 循环是 Java 提供的一个语法糖,内部使用迭代器来实现遍历操作,使得代码更加简洁和易读

3. 为什么增强for循环不支持在遍历集合时删除元素

3.1 问题排查

我们以文章最开始的例子为例

在这里插入图片描述

要想知道为什么增强 for 循环不支持在遍历集合时删除元素,我们需要一步步地排查,中间可能会涉及到阅读源码的过程


我们点击错误信息中给出的 ArrayList.java:1013

在这里插入图片描述

可以发现,当 modCount 变量和 expectedModCount 不相等时,就会抛出 ConcurrentModificationException 异常

在这里插入图片描述

哪里调用了 checkForComodification 方法呢,我们点击错误信息中给出的 ArrayList.java:967

在这里插入图片描述

在这里插入图片描述

可以发现,在用迭代器进行遍历时,每一次调用 next 方法都会调用一次 checkForComodification 方法,检查 modCount 和 expectedModCount 是否相等

那 modCount 和 expectedModCount 是从哪里来的呢

3.2 modCount 变量的来源

我们同时按下 CTRL 和 鼠标左键,查看 modCount 变量的来源,点击之后跳转到了 AbstractList 类的源码,而 ArrayList 继承了 AbstractList 类,说明 modCount 变量继承自 AbstractList 类

在这里插入图片描述

3.3 expectedModCount 变量的来源

那 expectedModCount 又是从哪里来的呢,按下 CTRL 和 鼠标左键,查看 modCount 变量的来源

可以发现,expectedModCount 变量来自于 ArrayList 的一个私有内部类 Itr,Itr 类实现了 Iterator 接口,而且 expectedModCount 变量的初值就是 modCount

在这里插入图片描述

3.4 导致modCount变量和expectedModCount不相等的原因

为什么 modCount 变量和 expectedModCount 不相等呢

我们查看 ArrayList 类 remove 方法的源码

在这里插入图片描述

remove 方法里面又调用了 fastRemove 方法

在这里插入图片描述

可以看到,fastRemove 方法在删除元素时会让 modCount 变量自增 1 个单位,导致 modCount 变量和 expectedModCount 变量不相等

3.5 为什么用迭代器遍历元素时删除元素不会报错

我们改成用迭代器遍历,在遍历过程中删除元素

在这里插入图片描述

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ListDeleteTests {

    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            arrayList.add(i);
        }

        Iterator<Integer> iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            Integer i = iterator.next();
            if (i % 2 == 0) {
                iterator.remove();
            }
        }

        arrayList.forEach(System.out::println);
    }

}

运行程序,可以发现没有报错,为什么没有报错呢,增强 for 循环不也是基于迭代器的吗


为了弄清楚这个问题,我们需要查看一下ArrayList 中迭代器的源码

ArrayList 的迭代器是一个名为 Itr 的内部私有类,该内部私有类继承了顶层的 Iterator 接口

可以看到,每次获取迭代器返回的都是一个新的迭代器对象,新的迭代器对象的 expectedModCount 变量的值与 modCount 相等

在这里插入图片描述

我们继续查看迭代器提供的 remove 方法,按下 CTRL 和 鼠标左键,查看 remove 方法的源码

在这里插入图片描述

在这里插入图片描述

可以发现,remove 是 Iterator 接口中的默认方法,调用该方法会抛出异常

既然调用 ArrayList 迭代器的 remove 方法没有报错,说明 ArrayList 的迭代器必然重写了 remove 方法

我们查看 ArrayList 内部迭代器具体的 remove 方法,发现 ArrayList 内部的迭代器确实重写了 remove 方法,而且调用了 ArrayList 类的 remove 方法,而且调用完 ArrayList 类的 remove 方法之后,将 expectedModCount 变量重新赋值为 modCount,这也是为什么使用迭代器遍历元素时删除元素不会报错的原因

在这里插入图片描述

3.6 遍历 Map 等集合时删除元素会抛出 ConcurrentModificationException吗

上述的测试都是针对 List 集合来说的,那其它类型的集合呢

我们以 HashMap 为例,测试遍历 Map 时删除元素会不会抛出 ConcurrentModificationException 异常

在这里插入图片描述

import java.util.HashMap;
import java.util.Map;

public class MapDeleteTests {

    public static void main(String[] args) {
        Map<String, Object> hashMap = new HashMap<>();

        for (int i = 0; i < 10; i++) {
            hashMap.put("key" + i, i);
        }

        for (String key : hashMap.keySet()) {
            if (Integer.parseInt(key.substring(3)) % 2 == 0) {
                hashMap.remove(key);
            }
        }

        hashMap.forEach((k, v) -> System.out.println(k + ":" + v));
    }

}

不出意外,还是抛出 ConcurrentModificationException 异常,抛出异常的原因与 List 类似

HashMap 内部会维护 modCount 变量

在这里插入图片描述

而 expectedModCount 来自于 HashMap 内部的一个名为 HashIterator 的抽象类

在这里插入图片描述

HashIterator 类有三个具体的子类,分别对应 HashMap 的三种遍历方式

在这里插入图片描述

4. 如何正确地在遍历集合时删除元素

上面只是说到在用增强 for 循环遍历集合时删除元素会抛出异常的原因,那如果我真的要在遍历集合时删除元素(或者说添加元素),该怎么做呢

4.1 使用迭代器进行删除(推荐使用)

可以使用迭代器来遍历并删除元素,这样不会抛出异常

在这里插入图片描述

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ListDeleteTests {

    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            arrayList.add(i);
        }

        Iterator<Integer> iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            Integer i = iterator.next();
            if (i % 2 == 0) {
                iterator.remove();
            }
        }

        arrayList.forEach(System.out::println);
    }

}

4.2 使用removeIf方法(推荐使用)

removeIf 方法的底层也是使用了迭代器,需要配合 lambda 表达式使用

removeIf 方法的源码如下(removeIf 方法来自 Collection 接口)

在这里插入图片描述

对于不同的集合来说,removeIf 方法的使用方式不同,总体可分为两类:

  • 实现了 Collection 接口的集合
  • 实现了 Map 接口的集合

4.2.1 实现了Collection接口的集合

以 List 集合为例

在这里插入图片描述

import java.util.ArrayList;
import java.util.List;

public class ListDeleteTests {

    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            arrayList.add(i);
        }

        arrayList.removeIf(i -> i % 2 == 0);

        arrayList.forEach(System.out::println);
    }

}

4.2.2 实现了Map接口的集合

以 HashMap 为例

在这里插入图片描述

import java.util.HashMap;
import java.util.Map;

public class MapDeleteTests {

    public static void main(String[] args) {
        Map<String, Object> hashMap = new HashMap<>();

        for (int i = 0; i < 10; i++) {
            hashMap.put("key" + i, i);
        }

        hashMap.keySet().removeIf(i -> Integer.parseInt(i.substring(3)) % 2 == 0);

        hashMap.forEach((k, v) -> System.out.println(k + ":" + v));
    }

}

4.3 收集要删除的元素,遍历结束之后再删除

在这里插入图片描述

import java.util.ArrayList;
import java.util.List;

public class ListDeleteTests {

    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            arrayList.add(i);
        }

        ArrayList<Integer> elementToDeleteList = new ArrayList<>();

        for (int i = 0; i < arrayList.size(); i++) {
            if (i % 2 == 0) {
                elementToDeleteList.add(i);
            }
        }

        elementToDeleteList.forEach(arrayList::remove);

        arrayList.forEach(System.out::println);
    }

}

5. 扩展:为什么用下标遍历元素集合时删除元素不会报错

对于可以用下标遍历的集合来说,可以在用下标遍历集合元素的同时删除元素,因为使用下标遍历集合并没有使用到迭代器

但是不建议在用下标遍历元素集合时删除元素,因为在我们的编程习惯中,用于遍历集合的下标都是只读的,如果我们修改了下标,可能会造成一些意想不到的问题

唯一需要注意的是,删除元素后下标要减一,否则会跳过某个元素

在这里插入图片描述

import java.util.ArrayList;
import java.util.List;

public class ListDeleteTests {

    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            arrayList.add(i);
        }

        for (int i = 0; i < arrayList.size(); i++) {
            if (arrayList.get(i) % 2 == 0) {
                arrayList.remove(i);
                i--;
            }
        }

        arrayList.forEach(System.out::println);
    }

}

http://www.kler.cn/a/328412.html

相关文章:

  • Python设计模式详解之5 —— 原型模式
  • 商业物联网详细指南:优势与挑战
  • 01_MinIO部署(Windows单节点部署/Docker化部署)
  • 机器学习基础04
  • 《Django 5 By Example》阅读笔记:p645-p650
  • vue项目使用eslint+prettier管理项目格式化
  • 【Verilog学习日常】—牛客网刷题—Verilog企业真题—VL69
  • 决策树中联合概率分布公式解释说明
  • 如何判断电器外壳是否带电
  • 十四、磁盘的管理
  • SpringBoot之Profile的两种使用方式
  • 二叉搜索树详解
  • 基于ARX结构的流密码算法Salsa20
  • mybatis-puls快速入门
  • Nginx的核心架构和设计原理
  • EnvoyFilter 是 Istio 中用于直接修改 Envoy 配置的一种资源类型
  • 帝都程序猿十二时辰
  • modelsim仿真 wave视图里 数据位宽和进制怎么显示
  • 通信工程学习:什么是CSMA/CD载波监听多路访问/冲突检测
  • 计算机知识科普问答--25(121-125)
  • 关于KKT条件的线性约束下非线性问题-MATLAB
  • 【机器学习】过拟合与欠拟合——如何优化模型性能
  • wx小程序中,商城订单详情显示还有多少分钟关闭
  • 「C++系列」模板
  • 项目实战:构建高效可扩展的Flask Web框架:集成Flask-SQLAlchemy、Marshmallow与日志管理
  • SpringBoot集成Redis及SpringCache缓存管理