博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java JDK源码分析之for循环删除异常原理以及解决办法
阅读量:7235 次
发布时间:2019-06-29

本文共 4935 字,大约阅读时间需要 16 分钟。

一、本地环境

  • 编辑器:IntelliJ IDEA 2017

  • JDK版本:jdk 1.8

二、引发的现象

在一些业务中,我们会用到遍历集合然后找到需要删除的元素进行remove,并且可能会删除多个元素。例如在管理群组时会踢人,踢人的业务逻辑大概是遍历这个群的人员关系,找到需要踢的人然后把他移除。

List
list = new ArrayList<>();list.add("a");list.add("b");list.add("c");list.add("d");for (String s : list) { if ("b".equals(s)) { list.remove(s); }}复制代码

比如这么一段代码,就会抛一下异常

java.util.ConcurrentModificationException	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)	at java.util.ArrayList$Itr.next(ArrayList.java:851)	at com.lonelycountry.test3.Test1.test3(Test1.java:68)复制代码

但是有的同学比较幸运,写了下面这段代码,巧妙或者说是碰巧躲避了这个陷阱

List
list = new ArrayList<>();list.add("a");list.add("b");list.add("c");list.add("d");for (String s : list) { if ("b".equals(s)) { list.remove(s); break; }}复制代码

三、解决方案

1、在只需要删一次集合内部元素的代码上加上break

List
list = new ArrayList<>();list.add("a");list.add("b");list.add("c");list.add("d");for (String s : list) { if ("b".equals(s)) { list.remove(s); break; }}复制代码

2、使用迭代器

List
list = new ArrayList<>();list.add("a");list.add("b");list.add("c");list.add("d");Iterator
iterator = list.iterator();while (iterator.hasNext()) { String s = iterator.next(); if ("b".equals(s)) { iterator.remove(); }}复制代码

四、查看源码找问题

1、遍历方法源码(这里会以ArrayList作为例子)

使用for循环遍历集合和使用迭代器遍历集合使用的是同一个方法,在ArrayList类中有个内部类实现了Iterator接口,有个叫next()的方法就是遍历方法(只截了部分方法)。

public class ArrayList
extends AbstractList
implements List
, RandomAccess, Cloneable, java.io.Serializable { private class Itr implements Iterator
{ 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]; } }}复制代码

next()方法第一行调用了checkForComodification()方法,这个方法是用来校验有没有非法操作的。

final void checkForComodification() {    //左边是操作次数,只要集合有了元素改变,就会modCount++,右边是预判操作数,    //大家可能会想到了,就是调用某个方法时预判操作数和实际操作数没有同步,结果就出问题了    if (modCount != expectedModCount)        throw new ConcurrentModificationException();}复制代码

2、ArrayList自身的remove(Object o)方法源码分析

public class ArrayList
extends AbstractList
implements List
, RandomAccess, Cloneable, java.io.Serializable { public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); //关注这个方法就行 return true; } } return false; } private void fastRemove(int index) { modCount++; //就是它导致的!!!! 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 }}复制代码

remove(Object o)方法中,遍历找到了需要删除的索引,校验索引有效后,执行了fastRemove(int index)方法。这个方法第一步就是对实际操作数+1,然后进行了数组操作,而这时expectedModCount没有变化,可想而知,当再执行next()方法的时候,checkForComodification()方法肯定会抛出异常。

public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos,     int length);复制代码

在群组操作上使用了System类的arraycopy()方法,底层调用的应该是C或者C++的方法(我猜的),我查了下文档,大概说下逻辑。

index(索引)srcPos开始取src数组的元素,长度为length,然后覆盖dest数组的destPos位置(注意是覆盖,不是插入)。但是为什么在操作群组上用这么复杂的方法我就不得而知了,有对算法精通的骚年可以指导下我哦。

3、ArrayList迭代器内部类的remove()方法源码分析

public class ArrayList
extends AbstractList
implements List
, RandomAccess, Cloneable, java.io.Serializable { private class Itr implements Iterator
{ 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(); } } }}复制代码

这个方法其实本质还是调用了ArrayList自身的remove(int index)方法,和上面的方法大同小异,但是在后面偷偷做了个实际操作数和预判操作数同步,就是因为这行逻辑导致使用迭代器遍历中删除不会抛异常。肯定有人疑惑既然知道了原因,为什么不在remove(Object o)方法中加一行同步呢,这是不行的,因为expectedModCount变量是内部类Itr中的变量,而remove(Object o)方法是ArrayList外部声明的,无法操作干扰内部类的变量

五、总结

主要分析了两种遍历集合的代码区别,以及出错的位置和原因。当然也有个疑惑就是在调用fastRemove(int index)方法时,为什么要使用System.arraycopy()方法。

转载地址:http://evlfm.baihongyu.com/

你可能感兴趣的文章
iOS开发: CocoaPods远程私有仓库的维护-添加子库
查看>>
【Python3网络爬虫开发实战】3-基本库的使用-3正则表达式
查看>>
支持获取 6.0+设备正在运行程序信息
查看>>
使用 Acorn 来解析 JavaScript
查看>>
如何下载、安装eclipse
查看>>
流计算框架 Flink 与 Storm 的性能对比
查看>>
JavaScript数据类型AND深拷贝和浅拷贝的不归路
查看>>
iOS逆向之旅(进阶篇) — HOOK(FishHook)
查看>>
Gradle 3.0.0设置Apk文件输出命名
查看>>
mac 使用php storm的基本配置
查看>>
装饰者模式
查看>>
集成计算引擎在大型企业绩效考核系统的应用方案
查看>>
150. Evaluate Reverse Polish Notation
查看>>
SpringBoot 实战 (十一) | 整合数据缓存 Cache
查看>>
css实现三栏布局的几种方法及优缺点
查看>>
proxychains是怎么工作的
查看>>
React16性能改善的原理一
查看>>
网页水印SDK的实现
查看>>
js的观察者模式
查看>>
函数柯里化
查看>>