千家信息网

绕过迭代器遍历时的数据修改异常的方法有哪些

发表于:2025-11-07 作者:千家信息网编辑
千家信息网最后更新 2025年11月07日,本篇内容主要讲解"绕过迭代器遍历时的数据修改异常的方法有哪些",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"绕过迭代器遍历时的数据修改异常的方法有哪些"吧!
千家信息网最后更新 2025年11月07日绕过迭代器遍历时的数据修改异常的方法有哪些

本篇内容主要讲解"绕过迭代器遍历时的数据修改异常的方法有哪些",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"绕过迭代器遍历时的数据修改异常的方法有哪些"吧!

前言

既然是绕过迭代器遍历时的数据修改异常,那么有必要先看一下是什么样的异常。如果在集合的迭代器遍历时尝试更新集合中的数据,比如像下面这样,我想输出 Hello,World,Java,迭代时却发现多了一个 C++ 元素,如果直接删除掉的话。

List list = new ArrayList<>();Collections.addAll(list, "Hello", "World", "C++", "Java");// 我想输出 Hello,World,Java,迭代时发现多一个 C++,所以直接删除掉。Iterator iterator = list.iterator();System.out.println(iterator.next());System.out.println(iterator.next());list.remove("C++");System.out.println(iterator.next());

那么我想你一定会遇到一个异常 ConcurrentModificationExceptio

HelloWorldjava.util.ConcurrentModificationException        at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:907)        at java.util.ArrayList$Itr.next(ArrayList.java:857)        at com.wdbyte.lab.jdk.ModCountDemo.updateCollections(ModCountDemo.java:26)

这个异常在刚开始学习 Java 或者使用其他的非线程安全的集合过程中可能都有遇到过。导致这个报错出现的原因就和我们操作的一样,对于某些集合,不建议在遍历时进行数据修改,因为这样会数据出现不确定性。

那么如何绕过这个错误呢?这篇文章中脑洞大开的三种方式一定不会让你失望。

异常原因

这不是一篇源码分析的文章,但是为了介绍绕过这个异常出现的原因,还是要提一下的,已经知道的同学可以直接跳过。

根据上面的报错,可以追踪到报错位置 ArrayList.java 的 857 行和 907 行,追踪源码可以发现在迭代器的 next 方法的第一行,调用了 checkForComodification() 方法。

而这个方法直接进行了一个把变量 modCountexpectedModCount 进行了对比,如果不一致就会抛出来 ConcurrentModificationException 异常。

final void checkForComodification() {    if (modCount != expectedModCount)        throw new ConcurrentModificationException();}

那么 modCount 这个变量存储的是什么信息呢?

/** * The number of times this list has been structurally modified. * Structural modifications are those that change the size of the * list, or otherwise perturb it in such a fashion that iterations in * progress may yield incorrect results. * * 

This field is used by the iterator and list iterator implementation * returned by the {@code iterator} and {@code listIterator} methods. * If the value of this field changes unexpectedly, the iterator (or list * iterator) will throw a {@code ConcurrentModificationException} in * response to the {@code next}, {@code remove}, {@code previous}, * {@code set} or {@code add} operations. This provides * fail-fast behavior, rather than non-deterministic behavior in * the face of concurrent modification during iteration. * *

Use of this field by subclasses is optional. If a subclass * wishes to provide fail-fast iterators (and list iterators), then it * merely has to increment this field in its {@code add(int, E)} and * {@code remove(int)} methods (and any other methods that it overrides * that result in structural modifications to the list). A single call to * {@code add(int, E)} or {@code remove(int)} must add no more than * one to this field, or the iterators (and list iterators) will throw * bogus {@code ConcurrentModificationExceptions}. If an implementation * does not wish to provide fail-fast iterators, this field may be * ignored. */protected transient int modCount = 0;

直接看源码注释吧,直接翻译一下意思就是说 modCount 数值记录的是列表的结构被修改的次数,结构修改是指那些改变列表大小的修改,或者以某种方式扰乱列表,从而使得正在进行的迭代可能产生不正确的结果。同时也指出了这个字段通常会在迭代器 iterator 和 listIterator 返回的结果中使用,如果 modCount 和预期的值不一样,会抛出 ConcurrentModificationException 异常。

而上面与 modCount 进行对比的字段 expectedModCount 的值,其实是在创建迭代器时,从 modCount 获取的值。如果列表结构没有被修改过,那么两者的值应该是一致的。

绕过方式一:40 多亿次循环绕过

上面分析了异常产生的位置和原因,是因为 modCount 的当前值和创建迭代器时的值有所变化。所以第一种思路很简单,我们只要能让两者的值一致就可以了。在源码 int modCount = 0; 中可以看到 modCount 的数据类型是 INT ,既然是 INT ,就是有数据范围,每次更新列表结构 modCount 都会增1,那么是不是可以增加到 INT 数据类型的值的最大值溢出到负数,再继续增加直到变回原来的值呢?如果可以这样,首先要有一种操作可以在更新列表结构的同时不修改数据。为此翻阅了源码寻找这样的方法。还真的存在这样的方法。

public void trimToSize() {    modCount++;    if (size < elementData.length) {        elementData = (size == 0)          ? EMPTY_ELEMENTDATA          : Arrays.copyOf(elementData, size);    }}

上来就递增了 modCount,同时没有修改任何数据,只是把数据的存储进行了压缩。

List list = new ArrayList<>();Collections.addAll(list, "Hello", "World", "C++", "Java");list.listIterator();Iterator iterator = list.iterator();System.out.println(iterator.next());System.out.println(iterator.next());list.remove("C++");// 40 多亿次遍历,溢出到负数,继续溢出到原值for (int n = Integer.MIN_VALUE; n < Integer.MAX_VALUE; n++) ((ArrayList) list).trimToSize();System.out.println(iterator.next());

正确输出了想要的 Hello,World,Java

绕过方式二:线程加对象锁绕过

分析一下我们的代码,每次输出的都是 System.out.println(iterator.next());。可以看出来是先运行了迭代器 next 方法,然后才运行了System.out 进行输出。所以第二种思路是先把第三个元素C++ 更新为Java ,然后启动一个线程,在迭代器再次调用 next 方法后,把第四个元素移除掉。这样就输出了我们想要的结果。

List list = new ArrayList<>();Collections.addAll(list, "Hello", "World", "C++", "Java");list.listIterator();Iterator iterator = list.iterator();System.out.println(iterator.next());System.out.println(iterator.next());// 开始操作list.set(2, "Java");Phaser phaser = new Phaser(2);Thread main = Thread.currentThread();new Thread(() -> {    synchronized (System.out) {        phaser.arriveAndDeregister();        while (main.getState() != State.BLOCKED) {            try {                Thread.sleep(100);            } catch (InterruptedException e) {                e.printStackTrace();            }        }        list.remove(3);    }}).start();phaser.arriveAndAwaitAdvance();System.out.println(iterator.next());// 输出集合System.out.println(list);/** * 得到输出 *  * Hello * World * Java * [Hello, World, Java] */

正确输出了想要的 Hello,World,Java 。这里简单说一下代码中的思路,Phaser 是 JDK 7 的新增类,是一个阶段执行处理器。构造时的参数 parties 的值为2,说明需要两个参与方完成时才会进行到下一个阶段。而 arriveAndAwaitAdvance 方法被调用时,可以让一个参与方到达。

所以线程中对 System.out 进行加锁,然后执行 arriveAndAwaitAdvance 使一个参与方报告完成,此时会阻塞,等到另一个参与方报告完成后,线程进入到一个主线程不为阻塞状态时的循环。

这时主线程执行 System.out.println(iterator.next()); 。获取到迭代器的值进行输出时,因为线程内的加锁原因,主线程会被阻塞。知道线程内把集合的最后一个元素移除,线程处理完成才会继续。

绕过方式三:利用类型擦除放入魔法对象

在创建集合的时候为了减少错误概率,我们会使用泛型限制放入的数据类型,其实呢,泛型限制的集合在运行时也是没有限制的,我们可以放入任何对象。所以我们可以利用这一点做些文章。

List list = new ArrayList<>();Collections.addAll(list, "Hello", "World", "C++", "Java");list.listIterator();Iterator iterator = list.iterator();System.out.println(iterator.next());System.out.println(iterator.next());// 开始操作((List)list).set(2, new Object() {    public String toString() {        String s = list.get(3);        list.remove(this);        return s;    }});System.out.println(iterator.next());

代码里直接把第三个元素放入了一个魔法对象,重写了 toString() 方法,内容是返回集合的第四个元素,然后删除第三个元素,这样就可以得到想要的 Hello,World,Java 输出。

到此,相信大家对"绕过迭代器遍历时的数据修改异常的方法有哪些"有了更深的了解,不妨来实际操作一番吧!这里是网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

迭代 数据 方法 线程 输出 C++ 元素 原因 方式 结构 参与方 对象 源码 类型 更新 一致 三个 代码 内容 同时 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 计算机软件开发要学多久 云服务器的医药管理软件 中行软件开发中心招聘人数 上海软件开发定制价格 如皋网络安全提示 数据库营销的过程 主播粉丝软件开发公司 管理win10服务器 华为入职网络安全考试 金蝶kis云数据库 长讯网络技术有限公司怎么样 邮储软件开发中心招聘信息 中国十大计算机软件开发公司 sqlite数据库 教程 软件开发太原公司 升级后的服务器不能启动 丹阳市苏菲亚网络技术有限公司 合肥市天气预报软件开发 roblox托马斯逃脱服务器名称 怎么查看服务器上的jvm 企业管理软件开发方案价钱 万方数据库收录图书期刊学位标准 英雄联盟登陆 无法连接服务器 移动公司网络安全教育总结 电脑网络服务器内存不足 成都大触网络技术有限公司电话 武装突袭3搭建服务器 最流行的互联网科技论坛 温州云淘网络技术有限公司 太仓品牌网络技术有哪些
0