每一个问题的出现都会有它的原因和背景。
List遍历何其简单,之所以会提出这个问题,是因为你可能会遇到过或者曾经写过类似下的代码:
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
list.forEach(v -> System.out.println(v));
for (String val : list) {
if (val.equals("a") || val.equals("b")) {
list.remove(val);
}
}
list.forEach(v -> System.out.println(v));
如果你是上面那样写,不出意外就会报ConcurrentModificationException:
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 com.wuling.test.App.main(App.java:23)
Process finished with exit code 1
报异常的原因,可参考另一篇文章,里面有详细描述。
链接: CME异常
那我们如何安全地在遍历过程中,移除掉list中的某个元素呢?
发生CME异常的根本原因是,在遍历过程中,你不能对List的元素进行增加、删减的改动,既然如此,可通过维护一个下标索引集合来达到删除效果,如下:
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
list.forEach(v -> System.out.println(v));
Stack<Integer> stack = new Stack();
int i = 0;
for (String val : list) {
if (val.equals("a") || val.equals("b")) {
stack.add(i);
}
i++;
}
while (!stack.empty()) {
int idx = stack.pop();
list.remove(idx);
}
System.out.println("移除元素后:");
list.forEach(v -> System.out.println(v));
执行结果:
a
b
c
d
e
移除元素后:
c
d
e
Process finished with exit code 0
方案一是可以实现遍历后,删除掉想要删除的元素,只是……是不是有一种多此一举的感觉?而且也并不是在遍历中删除,它是遍历后才删除,其实不算是符合要求,这里当做提供一种实现思路。
代码如下:
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
list.forEach(v -> System.out.println(v));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String val = iterator.next();
if (val.equals("a") || val.equals("b")) {
iterator.remove();
}
}
System.out.println("移除元素后:");
list.forEach(v -> System.out.println(v));
执行结果:
a
b
c
d
e
移除元素后:
c
d
e
Process finished with exit code 0
分析:
通过源码分析可以了解到,for遍历过程中,移除元素之所有会报ConcurrentModificationException异常,主要是因为for遍历的底层也是间接用到了Iterator,在Iterator在获取下一个元素时(next方法),会检查一下modCount是否等于expectedModCount,如果不等则抛出CME异常。而expectedModCount是初始迭代器时就赋值好且不会改变了的,但是modCount会在list调用remove、add、clear方法后会改变,也因此会造成modCount与expectedModCount的不相等,进而抛出异常。下面简单搬一下官方源码:
ArrayList -> Itr#next():
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];
}
ArrayList -> Itr#checkForComodification():
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
ArrayList#remove():
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
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
return oldValue;
}
那为何直接使用Iterator遍历,然后移除就不会抛出异常了呢?无需多言,看下面一小段源码你就明白了。
ArrayList -> Itr#remove():
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();
}
}
检查是否抛出异常的时机是在增删元素后的下一次调用next()时,而直接调用Iterator的remove方法,expectedModCount又被重新赋值为modCount了,因此,在下一次next()时expectedModCount仍始终等于modCount,也就不会抛出异常了。
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
list.forEach(v -> System.out.println(v));
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals("a") || list.get(i).equals("b")) {
list.remove(i);
i = i - 1;
}
}
System.out.println("移除元素后:");
list.forEach(v -> System.out.println(v));
执行结果也能如期正常。这种方案关键在于在list.remove(i)
之后,要对i减1,即i = i - 1;
,这也是平时容易犯的一个错误。
因为list移除一个元素后,其内部所有元素的下标索引都比原来变小了,看图就明白了:
在移除a之后,b原本的下标1就变成了0,但是在for循环中,i的值已经递增到了1,这时b就会跳过判等检测,因此我们需要把i减去1,避免在移除元素之后,漏掉某个元素的遍历或检查。
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
list.forEach(v -> System.out.println(v));
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i).equals("a") || list.get(i).equals("b")) {
list.remove(i);
}
}
System.out.println("移除元素后:");
list.forEach(v -> System.out.println(v));
执行结果也是正确的,逆序for循环处理就不需要i = i - 1;
了,因为即使循环中删除了某个元素,删除前和删除后的元素下标索引没有改变,如图示:
在遍历元素同时删除元素,这个操起本身很恶心,很容易出现数据上的各种问题或者奇怪现象。但是,或多或少,还真的就有这种实际业务需求,看似平常又简单的操作,对于经验不足的人来说,可能很难考虑周到,进而引发各种BUG,希望本文能帮助到你~
除了以上四种方法,你还能想到那些方法吗?
本文到这里就结束了咯~