1 前言
基础不牢,地动山摇!!!刚学Java基础那会,使用list循环删除总是搞不清楚哪种方式是正确的,只有运行代码去尝试每一种写法才知道结果,写多了以后就记住了要用迭代器的方式去删除,但是只知其然,而不知其所以然,偶尔有那么一两次翻开源码查看其原理,却总是蜻蜓点水,过一段时间又忘了,所以现在决定记录一下加深印象。
2 错误的遍历删除的方式
日常开发中最常用的集合可能就是ArrayList了,所以下面给出的例子都是以ArrayList示例,但集合类的遍历删除操作基本都相似,所以掌握了ArrayList的正确删除元素方式那么其他集合类就差不多掌握了。
2.1 for-each遍历删除方式
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
for (Integer e : list) {
if (e == 2) {
list.remove(e);
} else {
System.out.println(e);
}
}
这种方式是最常见的错误了,大部分初学者应该都犯过这样的错,它会抛出下面这个异常。
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)
细心的你应该从异常信息发现个比较奇怪的地方,为什么抛出的异常是ArrayList$Itr.next开始的?实际上这种方式的遍历只是个语法糖,只要是实现了Iterable接口就可以使用,但本质上是调用Iterable的iterator方法,之后返回了一个迭代器,调用hasNext和next方法,因此这个错是调用next方法抛出来的异常,我们继续从源码上分析一遍。
// 返回了一个迭代器
public Iterator<E> iterator() {
return new Itr();
}
iterator方法返回了一个ArrayList的内部类Itr
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
// 把modCount赋值给expectedModCount,modCount表示ArrayList被修改的次数,
// 也就是说调用remove和add方法时modCount的值会变
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
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];
}
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();
}
}
// 省略部分非重点源码。。。
final void checkForComodification() {
// modCount被修改时抛出异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
可以看到expectedModCount被赋予了modCount的初始值,在Itr的next和remove方法上都调用了checkForComodification方法,如果modCount被修改了就会抛出异常,但是看到这里是不是又有一个疑问,从上面的源码看不管调用next方法还是remove方法modCount的值都没有变啊,那从上面异常信息看不是调next方法时发生的吗?别急,我们慢慢说来,回到给出的迭代遍历删除的例子,实际上这里调用的是ArrayList的remove方法,这里才是关键!!我们看下ArrayList的remove方法源码。
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被修改了
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
}
当调用ArrayList的remove方法时,modCount会被修改,所以在例子中当代码执行了list.remove后,modCount的值就不等于expectedModCount了,因此等下一次循环调用next方法时就会抛出异常。你以为到这里就完了吗?No、No、No,在这里我还想补充的一点,当删除的是倒数第二个元素时,还会抛出这样的异常吗?答案是否定的。
public boolean hasNext() {
// cursor不等于size返回true,size是ArrayList的成员变量,
// 调用一次ArrayList的remove方法会减小1,cursor是Itr的成员变量。每次调用next方法就会加1
return cursor != size;
}
这里关键看hasNext方法,删除倒数第二个元素后,size就减1了,此时的cursor是等于size的,因此这时hasNext方法就返回false,不会往下执行了。最后再提个问题,当删除的是最后一个元素时,会不会抛出异常?可以自行分析下(别嫌我啰嗦,细节才是魔鬼)
2.2 for i 遍历删除方式
通过上面的分析,我们知道list for-each遍历删除会抛出ConcurrentModificationException异常,下面的方式不会抛出异常,因为这种方式并不会调用迭代器的next方法。
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
for (int i = 0; i < list.size(); i++) {
String e = list.get(i);
if (e.equals("b")) {
list.remove(e);
} else {
System.out.println(e);
}
}
虽然并不会抛出异常,但也不是完美的,这个例子list的size等于4,当删除元素b时,list.size就变为3,此时的i等于1,下次循环i已经变成2了,直接跳过了c(c的下标变成2了),所以结果没有打印元素c,开发中这种方式我们也是要避免的。
2.3 forEach lambda方法遍历删除方式
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.forEach(e -> {
if (e.equals("b")) {
list.remove(e);
} else {
System.out.println(e);
}
});
这种方式也会抛出异常,直接来看源码
@Override
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
代码的最后一步判断modCount是否等于expectedModCount,不等于则抛出异常。
3 正确的遍历删除的方式
上面分析了几种错误的遍历删除方式,下面看看怎样才是正确的。
3.1 迭代器Iterator遍历删除的方式
ArrayList<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 next = iterator.next();
if (next.equals("b")) {
iterator.remove();
} else {
System.out.println(next);
}
}
在2.1中已经对迭代器的方式分析过了,iterator.remove()和ArrayList.remove()是有区别的,前者modCount是不会变的,后者是会变的,至于为什么这样设计,我自己也还没想明白。
3.1 Stream过滤的方式
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
List<String> newList = list.stream().filter(e -> !e.equals("2")).collect(Collectors.toList());
for (String e : newList) {
System.out.println(e);
}
jdk8或以上版本用的最多的应该是这种方式了,也是我个人比较推荐的一种方式,返回新的list,无副作用。stream的源码比较复杂,不是本文分析的重点,以后有时间再研究研究。
4 总结
下面通过一张脑图来总结本文的内容