一、本地环境
-
编辑器:IntelliJ IDEA 2017
-
JDK版本:jdk 1.8
二、引发的现象
在一些业务中,我们会用到遍历集合然后找到需要删除的元素进行remove,并且可能会删除多个元素。例如在管理群组时会踢人,踢人的业务逻辑大概是遍历这个群的人员关系,找到需要踢的人然后把他移除。
Listlist = 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)复制代码
但是有的同学比较幸运,写了下面这段代码,巧妙或者说是碰巧躲避了这个陷阱
Listlist = 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
Listlist = 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、使用迭代器
Listlist = 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 ArrayListextends 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 ArrayListextends 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 ArrayListextends 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()方法。