一、本地环境
-
编辑器:IntelliJ IDEA 2017
-
JDK版本:jdk 1.8
二、引发的现象
在一些业务中,我们会用到遍历集合然后找到需要删除的元素进行remove,并且可能会删除多个元素。例如在管理群组时会踢人,踢人的业务逻辑大概是遍历这个群的人员关系,找到需要踢的人然后把他移除。
List
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.ConcurrentModificationExceptionat 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.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.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.add("a");
list.add("b");
list.add("c");
list.add("d");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 &#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];}}
}
next()
方法第一行调用了checkForComodification()
方法&#xff0c;这个方法是用来校验有没有非法操作的。
final void checkForComodification() {//左边是操作次数&#xff0c;只要集合有了元素改变&#xff0c;就会modCount&#43;&#43;&#xff0c;右边是预判操作数&#xff0c;//大家可能会想到了&#xff0c;就是调用某个方法时预判操作数和实际操作数没有同步&#xff0c;结果就出问题了if (modCount !&#61; 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 &#61;&#61; null) {for (int index &#61; 0; index
}
在remove(Object o)
方法中&#xff0c;遍历找到了需要删除的索引&#xff0c;校验索引有效后&#xff0c;执行了fastRemove(int index)
方法。这个方法第一步就是对实际操作数&#43;1
&#xff0c;然后进行了数组操作&#xff0c;而这时expectedModCount
没有变化&#xff0c;可想而知&#xff0c;当再执行next()
方法的时候&#xff0c;checkForComodification()方
法肯定会抛出异常。
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
在群组操作上使用了System
类的arraycopy()
方法&#xff0c;底层调用的应该是C
或者C&#43;&#43;
的方法&#xff08;我猜的&#xff09;&#xff0c;我查了下文档&#xff0c;大概说下逻辑。
从
index(索引)
为srcPos
开始取src
数组的元素&#xff0c;长度为length
&#xff0c;然后覆盖到dest
数组的destPos
位置&#xff08;注意是覆盖&#xff0c;不是插入&#xff09;。但是为什么在操作群组上用这么复杂的方法我就不得而知了&#xff0c;有对算法精通的骚年可以指导下我哦。
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 &#61; lastRet;lastRet &#61; -1;expectedModCount &#61; modCount;} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}}}
}
这个方法其实本质还是调用了ArrayList
自身的remove(int index)
方法&#xff0c;和上面的方法大同小异&#xff0c;但是在后面偷偷做了个实际操作数和预判操作数同步&#xff0c;就是因为这行逻辑导致使用迭代器遍历中删除不会抛异常。肯定有人疑惑既然知道了原因&#xff0c;为什么不在remove(Object o)
方法中加一行同步呢&#xff0c;这是不行的&#xff0c;因为expectedModCount
变量是内部类Itr
中的变量&#xff0c;而remove(Object o)
方法是ArrayList
类外部声明的&#xff0c;无法操作干扰内部类的变量。
五、总结
主要分析了两种遍历集合的代码区别&#xff0c;以及出错的位置和原因。当然也有个疑惑就是在调用fastRemove(int index)方法时&#xff0c;为什么要使用System.arraycopy()方法。