热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

为什么不能在ArrayList的ForEach循环中删除元素

大家都知道,不能在ArrayList的For-Each循环中删除元素。在Java的入门教程中都会写上这条。可是为什么不能呢?若非要在for循环遍历中删

大家都知道,不能在ArrayList的For-Each循环中删除元素。在Java的入门教程中都会写上这条。

可是为什么不能呢?若非要在for循环遍历中删除元素会发现什么呢?

本着一颗好奇的心,一起来研究研究。

先说现象:

List list = new ArrayList();list.add("1");list.add("2");for (String temp : list) {if ("1".equals(temp)) {list.remove(temp);}}System.out.println(list);

试一下就知道,这段代码不会报错,会正常输出“[2]”

而当我们删除“2”时,却会出现异常;

Exception in thread "main" java.util.ConcurrentModificationExceptionat java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)at java.util.ArrayList$Itr.next(ArrayList.java:851)at com.fansion.MethodTest.main(MethodTest.java:49)

然后我们把数据弄多一点:

List list = new ArrayList();list.add("1");list.add("2");list.add("3");list.add("4");list.add("5");for (String temp : list) {if ("4".equals(temp)) {list.remove(temp);}}System.out.println(list);

发现一个很神奇的现象,那就是只有在删除倒数第二个元素时不会报错,其他情况下都会报错。

是不是很诡异?

更诡异是,若不使用foreach快速遍历,直接使用for(int i;i

List list = new ArrayList();list.add("1");list.add("2");list.add("3");list.add("4");list.add("5");for (int i = 0; i

到底是什么原因呢?

由于两种遍历的方式不一样,结果就不一样,可以推测出问题可能出现在For-Each遍历上。

为了弄清楚这个问题,我们一步步的分析Java中ArrayList在遍历和删除的源代码。

1)直接进入ArrayList的源代码可以发现,在1.5的版本后,ArrayList中创建了一个内部迭代器Itr,并实现了Iterator接口,而For-Each遍历正是基于这个迭代器的hasNext()和next()方法来实现的;

先看一下这个内部迭代器:

/*** An optimized version of AbstractList.Itr*/private class Itr implements Iterator {int cursor; // index of next element to returnint lastRet &#61; -1; // index of last element returned; -1 if no suchint expectedModCount &#61; modCount;public boolean hasNext() {return cursor !&#61; size;}&#64;SuppressWarnings("unchecked")public E next() {checkForComodification();int i &#61; cursor;if (i >&#61; size)throw new NoSuchElementException();Object[] elementData &#61; ArrayList.this.elementData;if (i >&#61; elementData.length)throw new ConcurrentModificationException();cursor &#61; i &#43; 1;return (E) elementData[lastRet &#61; i];}public void remove() {if (lastRet <0)throw new IllegalStateException();checkForComodification();try {ArrayList.this.remove(lastRet);cursor &#61; lastRet;lastRet &#61; -1;expectedModCount &#61; modCount;} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}}&#64;Override&#64;SuppressWarnings("unchecked")public void forEachRemaining(Consumer consumer) {Objects.requireNonNull(consumer);final int size &#61; ArrayList.this.size;int i &#61; cursor;if (i >&#61; size) {return;}final Object[] elementData &#61; ArrayList.this.elementData;if (i >&#61; elementData.length) {throw new ConcurrentModificationException();}while (i !&#61; size && modCount &#61;&#61; expectedModCount) {consumer.accept((E) elementData[i&#43;&#43;]);}// update once at end of iteration to reduce heap write trafficcursor &#61; i;lastRet &#61; i - 1;checkForComodification();}final void checkForComodification() {if (modCount !&#61; expectedModCount)throw new ConcurrentModificationException();}}

这里有两个变量需要注意&#xff1a;
一个是modCount&#xff1a;这个外部的变量&#xff0c;也就是ArrayList下的变量&#xff1a;

/*** 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.*….*/protected transient int modCount &#61; 0;

只贴前面一部分注释&#xff0c;注释说这个变量来记录ArrayList集合的修改次数&#xff0c;也说明了可能会和迭代器内部的期望值不一致&#xff1b;

另外一个是Itr的变量expectedModCount;
能过上面的代码可以看到在Itr创建时默认定义了 int expectedModCount &#61; modCount;

我们先只看remove的操作:

通过在if条件成立时remove(“3”)操作的断点&#xff0c;我们进入到ArrayList下的remove方法&#xff0c;注意这里并没有进入内部迭代器Itr的remove()方法【这里是产生异常的关键点】

public boolean remove(Object o) {if (o &#61;&#61; null) {for (int index &#61; 0; index

很显然&#xff0c;这里应该正常的走到了fastRemove()方法中&#xff1a;

/** Private remove method that skips bounds checking and does not* return the value removed.*/private void fastRemove(int index) {modCount&#43;&#43;;int numMoved &#61; size - index - 1;if (numMoved > 0)System.arraycopy(elementData, index&#43;1, elementData, index,numMoved);elementData[--size] &#61; null; // clear to let GC do its work}

这里可以看到在fastRemove()方法中通过modCount&#43;&#43; 自增了一次&#xff0c;而此时并没有改变内部迭代器Itr中的expectedModCount 的值&#xff1b;

我们再往下走&#xff0c;此会再迭代到下一个元素&#xff1b;
先会通过hasNext(){return cursor !&#61; size;}来判断是否还有元素&#xff0c;很显然&#xff0c;若删除前面的元素&#xff0c;此处一定会为true&#xff08;注意&#xff1a;若之前删除的是倒数第二个元素&#xff0c;此处的cursor就是最后一个索引值size()-1,而由于已成功删除一个元素&#xff0c;此处的siz也是原size()-1,两者相等&#xff0c;此处会返回false&#xff09;
而在调用next()方面来获取下一个元素时&#xff0c;可以看到在next()方法中先调用了checkForComodification()方法&#xff1a;

final void checkForComodification() {if (modCount !&#61; expectedModCount)throw new ConcurrentModificationException();}

很显然&#xff0c;此处的modCount已经比expectedModCount大1了&#xff0c;肯定不一样&#xff0c;if条件成立&#xff0c;抛出一个ConcurrentModificationException异常。

致此&#xff0c;我们大概理清了为什么在foreach快速遍历中删除元素会崩溃的原因。

总结一下&#xff1a;
1&#xff09;在使用For-Each快速遍历时&#xff0c;ArrayList内部创建了一个内部迭代器iterator&#xff0c;使用的是hasNext和next()方法来判断和取下一个元素。
2&#xff09;ArrayList里还保存了一个变量modCount&#xff0c;用来记录List修改的次数&#xff0c;而iterator保存了一个expectedModCount来表示期望的修改次数&#xff0c;在每个操作前都会判断两者值是否一样&#xff0c;不一样则会抛出异常&#xff1b;
3&#xff09;在foreach循环中调用remove()方法后&#xff0c;会走到fastRemove()方法&#xff0c;该方法不是iterator中的方法&#xff0c;而是ArrayList中的方法&#xff0c;在该方法中modCount&#43;&#43;; 而iterator中的expectedModCount并没有改变&#xff1b;
4&#xff09;再次遍历时&#xff0c;会先调用内部类iteator中的hasNext(),再调用next(),在调用next()方法时&#xff0c;会对modCount和expectedModCount进行比较&#xff0c;此时两者不一致&#xff0c;就抛出了ConcurrentModificationException异常。

而为什么只有在删除倒数第二个元素时程序没有报错呢&#xff1f;

因为在删除倒数第二个位置的元素后&#xff0c;开始遍历最后一个元素时&#xff0c;先会走到内部类iterator的hasNext()方法时&#xff0c;里面返回的是 return cursor !&#61; size; 此时cursor是最后一个索引值&#xff0c;即原size()-1,而由于已经删除了一个元素&#xff0c;该方法内的size也是原size()-1,故 return cursor !&#61; size&#xff1b;会返回false,直接退出for循环&#xff0c;程序便不会报错。

最后&#xff0c;通过源代码的判断&#xff0c;要在循环中删除元素&#xff0c;最好的方式还是直接拿到ArrayList对象下的迭代器list.iterator()&#xff0c;通过源码可以看到&#xff0c;该方法也就是直接把内部的迭代器返回出来

public Iterator iterator() {return new Itr();}

而该迭代器正是在For-Each快速遍历中使用的迭代器Itr。

博客地址&#xff1a;为什么不能在ArrayList的For-Each循环中删除元素



作者&#xff1a;南山伐木
链接&#xff1a;https://www.jianshu.com/p/58c4af5b97ff
來源&#xff1a;简书
简书著作权归作者所有&#xff0c;任何形式的转载都请联系作者获得授权并注明出处。


推荐阅读
  • 使用nodejs爬取b站番剧数据,计算最佳追番推荐
    本文介绍了如何使用nodejs爬取b站番剧数据,并通过计算得出最佳追番推荐。通过调用相关接口获取番剧数据和评分数据,以及使用相应的算法进行计算。该方法可以帮助用户找到适合自己的番剧进行观看。 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • 本文详细介绍了Spring的JdbcTemplate的使用方法,包括执行存储过程、存储函数的call()方法,执行任何SQL语句的execute()方法,单个更新和批量更新的update()和batchUpdate()方法,以及单查和列表查询的query()和queryForXXX()方法。提供了经过测试的API供使用。 ... [详细]
  • 本文介绍了在CentOS上安装Python2.7.2的详细步骤,包括下载、解压、编译和安装等操作。同时提供了一些注意事项,以及测试安装是否成功的方法。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 不同优化算法的比较分析及实验验证
    本文介绍了神经网络优化中常用的优化方法,包括学习率调整和梯度估计修正,并通过实验验证了不同优化算法的效果。实验结果表明,Adam算法在综合考虑学习率调整和梯度估计修正方面表现较好。该研究对于优化神经网络的训练过程具有指导意义。 ... [详细]
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • ASP.NET2.0数据教程之十四:使用FormView的模板
    本文介绍了在ASP.NET 2.0中使用FormView控件来实现自定义的显示外观,与GridView和DetailsView不同,FormView使用模板来呈现,可以实现不规则的外观呈现。同时还介绍了TemplateField的用法和FormView与DetailsView的区别。 ... [详细]
  • 本文讨论了Kotlin中扩展函数的一些惯用用法以及其合理性。作者认为在某些情况下,定义扩展函数没有意义,但官方的编码约定支持这种方式。文章还介绍了在类之外定义扩展函数的具体用法,并讨论了避免使用扩展函数的边缘情况。作者提出了对于扩展函数的合理性的质疑,并给出了自己的反驳。最后,文章强调了在编写Kotlin代码时可以自由地使用扩展函数的重要性。 ... [详细]
  • MyBatis多表查询与动态SQL使用
    本文介绍了MyBatis多表查询与动态SQL的使用方法,包括一对一查询和一对多查询。同时还介绍了动态SQL的使用,包括if标签、trim标签、where标签、set标签和foreach标签的用法。文章还提供了相关的配置信息和示例代码。 ... [详细]
author-avatar
冲动王子2502901503
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有