一、本地环境
-
编辑器:IntelliJ IDEA 2017
-
JDK版本:jdk 1.8
二、引发的现象
在一些业务中,我们会用到遍历集合然后找到需要删除的元素进行remove,并且可能会删除多个元素。例如在管理群组时会踢人,踢人的业务逻辑大概是遍历这个群的人员关系,找到需要踢的人然后把他移除。
List<String> 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<String> 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<String> 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<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
Iterator<String> 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<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private class Itr implements Iterator<E> {
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<E> extends AbstractList<E> implements List<E>, 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<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private class Itr implements Iterator<E> {
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()方法。