遍历删除list集合元素的几种写法及错误分析

在使用集合List的时候,时常会想要遍历删除符合条件的集合元素。如果我们直接通过for循环的方式进行遍历删除操作的话,代码可能会抛出ConcurrentModificationException异常或者数据不准确的问题。

示例一 (错误实例)

  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("b");
list.add("c");
System.out.println(list);
for (int i = 0; i < list.size(); i++) {
if ("b".equals(list.get(i))) {
list.remove(list.get(i));
}
}
System.out.println(list);
}

现有一个ArrayList,其中包含元素[a, b, b, c]。示例一用for循环遍历删除其中的元素b,执行之后结果如下:

1
2
原始集合:[a, b, b, c]
操作后集合:[a, b, c]

由结果可见,for循环操作在执行过程中虽然不会抛出异常,但结果依然含有元素b。简单分析下循环操作过程:

  • 第一次循环,此时i = 0list.size() = 4list.get(0)得到元素是a,不进入if语句。此时集合:[a, b, b, c]

  • 第二次循环,此时i = 1list.size() = 4list.get(1)得到元素是b,进入if语句,执行remove操作,此时i = 1list.size()= 3。 此时集合:[a, b, c]

  • 第三次循环,此时i = 2list.size() = 3list.get(2)得到元素是c,不进入if语句;此时集合:[a, b, c]

  • 退出循环。

循环过程中,remove操作导致listindex发生了变化,遍历元素的时候就跳过第二个b元素,导致没有正确的删除元素。这么看来使用for循环遍历删除元素是不靠谱的!

示例二 (错误实例)

  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("b");
list.add("c");
System.out.println("原始集合:" + list);
for(String s: list){
if ("b".equals(s)) {
list.remove(s);
}
}
System.out.println("操作后集合:"+ list);
}

同样的数据换了一种for循环写法,得到的执行结果是:

1
2
3
4
5
6
7
8
9
10
11
原始集合:[a, b, b, c]
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at me.lishuo.TestRmList.main(TestRmList.java:19)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

执行的过程中,抛出一个ConcurrentModificationException异常。追踪异常信息可知,该异常是ArrayListIterator抛出来的。由此可推测,这种语法糖写法的for循环是通过Iterator实现遍历的。编译一下源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] var0) {
ArrayList var1 = new ArrayList();
var1.add("a");
var1.add("b");
var1.add("b");
var1.add("c");
System.out.println("原始集合:" + var1);
Iterator var2 = var1.iterator();
while(var2.hasNext()) {
String var3 = (String)var2.next();
if("b".equals(var3)) {
var1.remove(var3);
}
}
System.out.println("操作后集合:" + var1);
}

如上代码,也证实了我的推测。下面就分析下,为啥通过iterator遍历,会抛出这儿异常。从异常的堆栈信息中可以得知,是在调用var2.next()时,抛出异常。看一下ArrayList.Itr的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
...
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];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
...
}

var1.iterator();创建Iterator的时候把当前的modCount赋值给expectedModCount。在后面调用next()方法的之前,都要检查modCount是否等于expectedModCount,不相等则抛出异常。而我们在遍历list的时候调用了remove方法,remove方法中有modCount++的操作,这就导致了modCount != expectedModCount,异常的根源所在。

之所以存在modCount,是因为ArrayList并非是线程安全的。modCount的存在就是为了防止用户在并发环境使用ArrayList

由此可知,通过这种方式的for循环也是无法正常遍历删除元素的。

示例三 (错误实例)

  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("b");
list.add("c");
System.out.println("原始集合:" + list);
list.stream().filter(s -> "b".equals(s)).forEach(list::remove);
System.out.println("操作后集合:"+ list);
}

这次换一种Java8 stream API进行遍历删除操作,得到的执行结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
原始集合:[a, b, b, c]
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1380)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
at me.lishuo.TestRmList.main(TestRmList.java:19)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

同样是抛了ConcurrentModificationException的异常,和示例二不同的是异常抛出位置不同。实际上原理和实例二相同,都是remove操作引起modCount的变化。

示例四 (正确写法)

  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("b");
list.add("c");
System.out.println("原始集合:" + list);
Iterator iterator = list.iterator();
while(iterator.hasNext()){
if ("b".equals(iterator.next())){
iterator.remove();
}
}
System.out.println("操作后集合:"+ list);
}
  • 结果
1
2
原始集合:[a, b, b, c]
操作后集合:[a, c]

通过Iterator进行遍历删除元素,才是正确的写法,之所以不会抛ConcurrentModificationException异常,是因为再执行iterator.remove()中将modCount重新赋值给expectedModCount;