Exception in thread "main" java.util.ConcurrentModificationException

本贴最后更新于 2388 天前,其中的信息可能已经时移世改

遍历 List 是我们在写代码中经常碰到的,有时候我们会碰到想在遍历的途中删掉某些元素的需求,但一不小心可能就会报快速失败错误(FastFail),这其实跟 List 当中的一些实现有关,下面我选取了两种情况,来自知乎大神的回答,加上自己的疑惑和总结。

作者:RednaxelaFX

链接:https://www.zhihu.com/question/56586732/answer/149650876

来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

总结下就是两种情况:一种是可以删除末尾的元素,另一种是在倒数第二时可以删除任何一个元素。

我觉得题主的首要问题是被自己写的代码(以及 JDK 的 ArrayList 那可恶的 API)给坑了。

ArrayList 上有两个版本的 remove 方法:

public E remove(int index)

public boolean remove(Object o)

题主很可能以为自己调用的是第二个版本,但实际上调用的是第一个版本——remove(3) 删除了位于末尾的元素,而不是位于倒数第二的元素。

ArrayList.iterator() 返回出来的 Iterator,里面的 hasNext()是不关心 modification count 的,而 next()会去检查 modification count:

List 中的内部类

/** * An optimized version of AbstractList.Itr */ private class Itr implements Iterator { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }

所以题主的那个程序实际做的事情就是:

  • 通过 ArrayList.iterator() 得到了一个新的 iterator,开始遍历

  • list.remove(3):删除了位于 index 3 的元素(Integer.valueOf(4) 得到的对象)

  • 然后调用 iterator.hasNext(),得到 false,于是就退出了循环而没有去执行那个会检查 modification count 的 next()方法。

ArrayList 的 JavaDoc 说:

The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.

Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.

没有特别指定 Iterator 里的哪些方法一定会根据 fail-fast 原则而抛异常。但 Iterator.hasNext()的 JavaDoc 说:

hasNext

boolean hasNext()

Returns true if the iteration has more elements. (In other words, returns true if next() would return an element rather than throwing an exception.)

Returns:true if the iteration has more elements

就这个规定来说,我觉得题主观察到的现象应该算是 JDK 实现的上的巧合:因为如果在这个位置调用 next()的话会抛 ConcurrentModificationException 异常,所以 hasNext()也要返回 false,于是就好啦。

但 JDK 这个具体实现看起来还是有 bug,应该让 hasNext()也做 checkForComodification()的不抛异常对应动作才对。

就这样。

还是未能解决下面的疑问:

在判断的是倒数第二个的时候,所有的元素都能删。

List list = new ArrayList(); list.add("a"); list.add("b"); list.add("c"); list.add("d"); list.add("e"); list.add("f"); Iterator it = list.iterator(); while (it.hasNext()) { String itt = (String) it.next(); if (itt.equals("e")) { list.remove("a"); } } for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); }

下面是解释:

作者:xRay

链接:https://www.zhihu.com/question/56916067/answer/151995061

来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

为什么没抛异常,跟下代码其实就一清二楚了。

ArralyList 有个版本号 modCount,每次修改 ArrayList 时版本号就会往上加。Iterator 里面也有个版本号 expectedModCount 它的初始值就是 modCount。只有 expectedModCount != modCount 才会抛异常。所以 printList 里面的绝对不会抛异常。那问题就出在进行 remove 的那个迭代器上,首先迭代器是在什么时候比较这两个值呢? 答案是在 remove 跟 next 的时候,也就是说 hasNext 它不会抛出这个异常。

List 中的删除方法:

/** * Removes the element at the specified position in this list. * Shifts any subsequent elements to the left (subtracts one from their * indices). * * @param index the index of the element to be removed * @return the element that was removed from the list * @throws IndexOutOfBoundsException {@inheritDoc} */ public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue; }

然后,看下题主的这段代码

Iterator iterator = list.iterator(); while(iterator.hasNext()) { Integer integer = iterator.next(); if(integer == 2) list.remove(integer); }

这里面调用了 List 的 remove 方法,所以它不会抛出 ConcurrentModificationException。问题是 remove 之后为什么 hasNext 会返回 false。我们看下 hasNext 方法

int cursor; // index of next element to return

public boolean hasNext() { return cursor != size; }

看注释,cursor 是指向一个元素的,也就是当 list.remove(integer=2)的时候,cursor 实际等于 2。而 list.remove(integer)后 size=3-1=2,所以 hasNext 返回 false,也就不会执行 next 方法,所以也就不会抛出 ConcurrentModificationException。

不过,这个问题,更有意思的 ArrayList 有两个 remove 方法

public E remove(int index) boolean remove(Object o)

当参数是 Integer 时会调用哪个? 我在 JLS 中找到这么一段,意思是选择重载函数时不会优先考虑装箱跟拆箱

The first phase (§15.12.2.2) performs overload resolution without permitting boxing or unboxing conversion, or the use of variable arity method invocation. If no applicable method is found during this phase then processing continues to the second phase.

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3203 引用 • 8217 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • visus

    同步写同步读,试一试用 COW 模式

推荐标签 标签

  • Markdown

    Markdown 是一种轻量级标记语言,用户可使用纯文本编辑器来排版文档,最终通过 Markdown 引擎将文档转换为所需格式(比如 HTML、PDF 等)。

    172 引用 • 1545 回帖
  • 服务器

    服务器,也称伺服器,是提供计算服务的设备。由于服务器需要响应服务请求,并进行处理,因此一般来说服务器应具备承担服务并且保障服务的能力。

    125 引用 • 585 回帖 • 2 关注
  • flomo

    flomo 是新一代 「卡片笔记」 ,专注在碎片化时代,促进你的记录,帮你积累更多知识资产。

    6 引用 • 143 回帖
  • Netty

    Netty 是一个基于 NIO 的客户端-服务器编程框架,使用 Netty 可以让你快速、简单地开发出一个可维护、高性能的网络应用,例如实现了某种协议的客户、服务端应用。

    49 引用 • 33 回帖 • 47 关注
  • GAE

    Google App Engine(GAE)是 Google 管理的数据中心中用于 WEB 应用程序的开发和托管的平台。2008 年 4 月 发布第一个测试版本。目前支持 Python、Java 和 Go 开发部署。全球已有数十万的开发者在其上开发了众多的应用。

    14 引用 • 42 回帖 • 827 关注
  • WebClipper

    Web Clipper 是一款浏览器剪藏扩展,它可以帮助你把网页内容剪藏到本地。

    3 引用 • 9 回帖 • 1 关注
  • 创造

    你创造的作品可能会帮助到很多人,如果是开源项目的话就更赞了!

    187 引用 • 1021 回帖
  • Scala

    Scala 是一门多范式的编程语言,集成面向对象编程和函数式编程的各种特性。

    13 引用 • 11 回帖 • 160 关注
  • IDEA

    IDEA 全称 IntelliJ IDEA,是一款 Java 语言开发的集成环境,在业界被公认为最好的 Java 开发工具之一。IDEA 是 JetBrains 公司的产品,这家公司总部位于捷克共和国的首都布拉格,开发人员以严谨著称的东欧程序员为主。

    181 引用 • 400 回帖
  • 浅吟主题

    Jeffrey Chen 制作的思源笔记主题,项目仓库:https://github.com/TCOTC/Whisper

    2 引用 • 32 回帖 • 1 关注
  • LaTeX

    LaTeX(音译“拉泰赫”)是一种基于 ΤΕΧ 的排版系统,由美国计算机学家莱斯利·兰伯特(Leslie Lamport)在 20 世纪 80 年代初期开发,利用这种格式,即使使用者没有排版和程序设计的知识也可以充分发挥由 TeX 所提供的强大功能,能在几天,甚至几小时内生成很多具有书籍质量的印刷品。对于生成复杂表格和数学公式,这一点表现得尤为突出。因此它非常适用于生成高印刷质量的科技和数学类文档。

    12 引用 • 59 回帖 • 1 关注
  • wolai

    我来 wolai:不仅仅是未来的云端笔记!

    2 引用 • 14 回帖 • 1 关注
  • Rust

    Rust 是一门赋予每个人构建可靠且高效软件能力的语言。Rust 由 Mozilla 开发,最早发布于 2014 年 9 月。

    59 引用 • 22 回帖 • 1 关注
  • H2

    H2 是一个开源的嵌入式数据库引擎,采用 Java 语言编写,不受平台的限制,同时 H2 提供了一个十分方便的 web 控制台用于操作和管理数据库内容。H2 还提供兼容模式,可以兼容一些主流的数据库,因此采用 H2 作为开发期的数据库非常方便。

    11 引用 • 54 回帖 • 674 关注
  • AWS
    11 引用 • 28 回帖 • 8 关注
  • SpaceVim

    SpaceVim 是一个社区驱动的模块化 vim/neovim 配置集合,以模块的方式组织管理插件以
    及相关配置,为不同的语言开发量身定制了相关的开发模块,该模块提供代码自动补全,
    语法检查、格式化、调试、REPL 等特性。用户仅需载入相关语言的模块即可得到一个开箱
    即用的 Vim-IDE。

    3 引用 • 31 回帖 • 114 关注
  • V2Ray
    1 引用 • 15 回帖 • 5 关注
  • 持续集成

    持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

    15 引用 • 7 回帖 • 3 关注
  • Logseq

    Logseq 是一个隐私优先、开源的知识库工具。

    Logseq is a joyful, open-source outliner that works on top of local plain-text Markdown and Org-mode files. Use it to write, organize and share your thoughts, keep your to-do list, and build your own digital garden.

    7 引用 • 69 回帖 • 2 关注
  • TextBundle

    TextBundle 文件格式旨在应用程序之间交换 Markdown 或 Fountain 之类的纯文本文件时,提供更无缝的用户体验。

    1 引用 • 2 回帖 • 82 关注
  • 区块链

    区块链是分布式数据存储、点对点传输、共识机制、加密算法等计算机技术的新型应用模式。所谓共识机制是区块链系统中实现不同节点之间建立信任、获取权益的数学算法 。

    92 引用 • 752 回帖
  • 生活

    生活是指人类生存过程中的各项活动的总和,范畴较广,一般指为幸福的意义而存在。生活实际上是对人生的一种诠释。生活包括人类在社会中与自己息息相关的日常活动和心理影射。

    230 引用 • 1432 回帖
  • 博客

    记录并分享人生的经历。

    273 引用 • 2389 回帖 • 1 关注
  • 百度

    百度(Nasdaq:BIDU)是全球最大的中文搜索引擎、最大的中文网站。2000 年 1 月由李彦宏创立于北京中关村,致力于向人们提供“简单,可依赖”的信息获取方式。“百度”二字源于中国宋朝词人辛弃疾的《青玉案·元夕》词句“众里寻他千百度”,象征着百度对中文信息检索技术的执著追求。

    63 引用 • 785 回帖 • 66 关注
  • uTools

    uTools 是一个极简、插件化、跨平台的现代桌面软件。通过自由选配丰富的插件,打造你得心应手的工具集合。

    7 引用 • 28 回帖
  • OnlyOffice
    4 引用 • 19 关注
  • Vue.js

    Vue.js(读音 /vju ː/,类似于 view)是一个构建数据驱动的 Web 界面库。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。

    268 引用 • 666 回帖