我们都知道集合循环删除元素,要使用iterator和while循环,不能用for和foreach。
foreach会抛出异常java.util.ConcurrentModificationException,具体原因是什么呢?
先来看一段代码,摘自阿里巴巴的java开发手册
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
for (String temp : list) {
if("1".equals(temp)){
list.remove(temp);
}
}
此时执行代码,没有问题,但是需要注意,循环此时只执行了一次。具体过程后面去分析。再来看一段会出问题的代码:
List<String> list = new ArrayList<String>();
list .add("1");
list .add("2");
for (String temp : list ) {
if("2".equals(temp)){
list .remove(temp);
}
}
输出为:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at MyTest.test(MyTest.java:32)
at MyTest.main(MyTest.java:25)
是不是很奇怪?接下来将class文件,反编译下,结果如下
1 List list = new ArrayList();
2 list.add("1");
3 list.add("2");
4 Iterator i$ = list.iterator();
5 do
6 {
7 if(!i$.hasNext())
8 break;
9 String temp = (String)i$.next();
10 if("1".equals(temp))
11 list.remove(temp);
12 } while(true);
几个需要注意的点:
1.foreach遍历集合,实际上内部使用的是iterator。
2.代码先判断是否hasNext,然后再去调用next,这两个函数是引起问题的关键。
3.这里的remove还是list的remove方法。
先去观察下list.remove()方法中的核心方法fastRemove()方法。
1 private void fastRemove(int index) {
2 modCount++;
3 int numMoved = size - index - 1;
4 if (numMoved > 0)
5 System.arraycopy(elementData, index+1, elementData, index,
6 numMoved);
7 elementData[--size] = null; // clear to let GC do its work
8 }
注意第二行,modCount++,此处先不表,下文再说这个参数。
顺路观察下list.add()方法
1 public boolean add(E e) {
2 ensureCapacityInternal(size + 1); // Increments modCount!!
3 elementData[size++] = e;
4 return true;
5 }
注意第二行的注释,说明这个方法也会使modCount++
再去观察下,iterator()方法
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
//游标记录当前索引的位置,是从0开始的
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;//记录元素修改记录,若在迭代List时,modCount发生变化将会抛出ConcurrentModificationException异常
//判断游标是否到达最后的位置,若没有表示还有元素,若有则没有元素了
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
//校验是否被修改(其实这里存在多线程问题,所以说ArrayList不是线程安全的)
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;//游标向后移动1
return (E) elementData[lastRet = i];//返回值并且赋值
}
public void remove() {
//lastRet初始值为-1,若需要调用remove方法,则bi
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
//删除lastRet索引处的元素
ArrayList.this.remove(lastRet);
//游标前移,由于是删除remove方法删除的是lastRet位置的元素则游标需要前移才能保证可以遍历到所有的元素
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
//检查列表是否修改,若修改则抛出异常
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
几个需要注意的点:
1.在iterator初始化的时候(也就是for循环开始处),expectedModCount = modCount,猜测是和当时list内部的元素数量有关系(已证实)。
2.当cursor != size的时候,hasNext返回true
3.next()函数的第一行,checkForComodification()这个函数就是报错的原因 这个函数就是万恶之源
4.checkForComodification方法中,mod != expectedModCount 就会抛出ConcurrentModificationException()
接下来分析文章开头的第一个例子,为啥不会报错?
第一个例子
初始化:expectedModCount = modCount = 2;cursor = 0; size = 2
第一次循环:list.remove()调用fastRemove(), modCount++,size–;
执行完第一次循环后:expectedModCount =2,modCount =3,cursor = 1,size = 1 执行hasNext()返回false,程序不会报错,但只执行了一次循环。
第二个例子
初始化:expectedModCount = modCount = 2;cursor = 0; size = 2
第一次循环:无操作
执行完第一次循环后:expectedModCount =2,modCount =2,cursor = 1,size = 2 执行hasNext()返回true,进入下次循环。
第二次循环:list.remove()调用fastRemove(), modCount++,size–;
执行完第二次循环后:expectedModCount =2,modCount =3,cursor = 2, size = 1 此时cursor != size,hasNext()返回true,继续执行循环,调用next方法,mod != expectedModCount ,抛出异常ConcurrentModificationException
至此,关于为什么foreach循环中remove抛异常ConcurrentModificationException,原因已经很明确了。
那为什么使用iterator和while,调用iterator.remove就没问题呢,其实源码里已经很明确了,iterator自带remove每次执行完毕都会重置expectedModCount。
手册上推荐的代码如下
1 Iterator<String> it = a.iterator(); while(it.hasNext()){
2 String temp = it.next(); if(删除元素的条件){
3 it.remove();
4 }
5 }
此时remove是iterator的remove,我们看一下它的源码:
public void remove() {
//lastRet初始值为-1,若需要调用remove方法,则bi
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
//删除lastRet索引处的元素
ArrayList.this.remove(lastRet);
//游标前移,由于是删除remove方法删除的是lastRet位置的元素则游标需要前移才能保证可以遍历到所有的元素
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
注意第11-14行,每次删除后cursor前移一位,expectedModCount = modCount,所以此时程序不会有之前的问题。
但是手册上推荐的方法,在多线程环境还是有可能出现问题,一个线程执行上面的代码,一个线程遍历迭代器中的元素,同样会抛出CocurrentModificationException。
如果要并发操作,需要对iterator对象加锁。
拓展:
1.modCount是指ArrayList的修改次数,每次add或remove都会自增,
当迭代时,就是将这个modCount暂存在expectedModCount中,
每次获取下一个元素时,都检查下修改次数是否有变动,有变动则不再继续迭代,而是抛出错误ConcurrentModificationException
这样就强制要求在迭代时不能进行remove/add操作,而foreach会编译成迭代,所以foreach时也不能进行remove/add操作
2 再看一个例子
public static void main(String[] args) throws Exception {
List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
for (String temp : a) {
System.out.println(temp);
if("2".equals(temp)){
a.add("3");
a.remove("2");
}
}
}
此时输出为:
1
2
显然,程序并没有执行第三次循环,第二次循环结束,cursor再一次等于size,程序退出循环。
与remove类似,将文章开头的代码中remove替换为add,我们会发现无论是第一个例子还是第二个例子,都会抛出ConcurrentModificationException错误,原因同上。
3.for循环正向遍历不抛异常,但每次remove元素,后面的元素下标 -1,每次循环 i++,会导致遍历不完整,进而导致删除不完全,解决思路:
1.list.remove之后不执行 i++
List<Integer> list = new ArrayList<Integer>();
list.add(2);
list.add(1);
list.add(1);
list.add(3);
for (int i = 0; i < list.size(); i++) {
Integer integer = list.get(i);
if (integer == 1){
list.remove(i);
i--;
}
}
2.for反向遍历。
for (int i = list.size()-1; i >= 0; i--) {
Integer integer = list.get(i);
if (integer == 1){
list.remove(i);
}
}
以上两种都是可以的,但是太low,且每次remove之后,i后面的元素都要换下标,效率不好,不推荐使用。