阿里面试官:为什么Java开发手册强制不要在 foreach 里进行元素删除?

46 阅读4分钟

image.png

首先来分析反例,反编译后的代码

List<String> list = new ArrayList();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");
Iterator var2 = list.iterator();

while(var2.hasNext()) {
    String str = (String)var2.next();
    if ("沉默王二".equals(str)) {
        list.remove(str);
    }
}

System.out.println(list);

ArrayList 实现了Itertor接口

public Iterator<E> iterator() {
    return new ArrayList.Itr();
}

Itr是其中的一个内部类,用来真正的进行遍历等操作。

private class Itr implements Iterator<E> {
    int cursor;
    int lastRet = -1;
    int expectedModCount;

    Itr() {
        this.expectedModCount = ArrayList.this.modCount;
    }

    public boolean hasNext() {
        return this.cursor != ArrayList.this.size;
    }

    public E next() {
        this.checkForComodification();
        int i = this.cursor;
        if (i >= ArrayList.this.size) {
            throw new NoSuchElementException();
        } else {
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            } else {
                this.cursor = i + 1;
                return elementData[this.lastRet = i];
            }
        }
    }

    public void remove() {
        if (this.lastRet < 0) {
            throw new IllegalStateException();
        } else {
            this.checkForComodification();

            try {
                ArrayList.this.remove(this.lastRet);
                this.cursor = this.lastRet;
                this.lastRet = -1;
                this.expectedModCount = ArrayList.this.modCount;
            } catch (IndexOutOfBoundsException var2) {
                throw new ConcurrentModificationException();
            }
        }
    }

    public void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        int size = ArrayList.this.size;
        int i = this.cursor;
        if (i < size) {
            Object[] es = ArrayList.this.elementData;
            if (i >= es.length) {
                throw new ConcurrentModificationException();
            }

            while(i < size && ArrayList.this.modCount == this.expectedModCount) {
                action.accept(ArrayList.elementAt(es, i));
                ++i;
            }

            this.cursor = i;
            this.lastRet = i - 1;
            this.checkForComodification();
        }

    }

    final void checkForComodification() {
        if (ArrayList.this.modCount != this.expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
}

元素报错案例

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");

for (String str : list) {
	if ("沉默王二".equals(str)) {
		list.remove(str);
	}
}

System.out.println(list);

显示这个错误,深入探讨一下

image.png

image.png image.png

即在重写了iterator的arraylist的Itr类的remove方法前会先判断修改次数与期望修改的次数是否相同,不相同就会报出该错误
由于 list 此前执行了 3 次 add 方法。

  • add 方法调用 ensureCapacityInternal 方法
  • ensureCapacityInternal 方法调用 ensureExplicitCapacity 方法
  • ensureExplicitCapacity 方法中会执行 modCount++
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
}

所以 modCount 的值在经过三次 add 后为 3,于是 new Itr() 后 expectedModCount 的值也为 3(回到前面去看一下 Itr 的源码)。

接着来执行 for-each 的循环遍历。

执行第一次循环时,发现“沉默王二”等于 str,于是执行 list.remove(str)

  • remove 方法调用 fastRemove 方法
  • fastRemove 方法中会执行 modCount++
private void fastRemove(int index) {
    modCount++;
}

modCount 的值变成了 4。

第二次遍历时,会执行 Itr 的 next 方法(String str = (String) var3.next();),next 方法就会调用 checkForComodification 方法。

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

此时 expectedModCount 为 3,modCount 为 4,就只好抛出 ConcurrentModificationException 异常了。

使用Iterator

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");

Iterator<String> itr = list.iterator();

while (itr.hasNext()) {
	String str = itr.next();
	if ("沉默王二".equals(str)) {
		itr.remove();
	}
}

这个为什么没有问题? 因为

image.png

使用iteartor的remove方法最后会将这两个值变为相同的,下一次next时检查也不会出错。 为什么不能在foreach里执行删除操作?

因为 foreach 循环是基于迭代器实现的,而迭代器在遍历集合时会维护一个 expectedModCount 属性来记录集合被修改的次数。如果在 foreach 循环中执行删除操作会导致 expectedModCount 属性值与实际的 modCount 属性值不一致,从而导致迭代器的 hasNext() 和 next() 方法抛出 ConcurrentModificationException 异常。

为了避免这种情况,应该使用迭代器的 remove() 方法来删除元素,该方法会在删除元素后更新迭代器状态,确保循环的正确性。如果需要在循环中删除元素,应该使用迭代器的 remove() 方法,而不是集合自身的 remove() 方法。

就像这样。

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");

Iterator<String> itr = list.iterator();

while (itr.hasNext()) {
	String str = itr.next();
	if ("沉默王二".equals(str)) {
		itr.remove();
	}
}

除此之外,我们还可以采用 Stream 流的filter() 方法来过滤集合中的元素,然后再通过 collect() 方法将过滤后的元素收集到一个新的集合中。

List<String> list = new ArrayList<>(Arrays.asList("沉默", "王二", "陈清扬"));
list = list.stream().filter(s -> !s.equals("陈清扬")).collect(Collectors.toList());

同时也可以使用以下两种方法

1)remove 后 break
List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");

for (String str : list) {
	if ("沉默王二".equals(str)) {
		list.remove(str);
		break;
	}
}

break 后循环就不再遍历了,意味着 Iterator 的 next 方法不再执行了,也就意味着 checkForComodification 方法不再执行了,所以异常也就不会抛出了。

但是呢,当 List 中有重复元素要删除的时候,break 就不合适了。

2)for 循环
List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");
for (int i = 0; i < list.size(); i++) {
	String str = list.get(i);
	if ("沉默王二".equals(str)) {
		list.remove(str);
	}
}

for 循环虽然可以避开 fail-fast 保护机制,也就说 remove 元素后不再抛出异常;但是呢,这段程序在原则上是有问题的。为什么呢?

第一次循环的时候,i 为 0,list.size() 为 3,当执行完 remove 方法后,i 为 1,list.size() 却变成了 2,因为 list 的大小在 remove 后发生了变化,也就意味着“沉默王三”这个元素被跳过了。能明白吗?

remove 之前 list.get(1) 为“沉默王三”;但 remove 之后 list.get(1) 变成了“一个文章真特么有趣的程序员”,而 list.get(0) 变成了“沉默王三”

可以考虑一下for循环如果解决这个问题,从后往前。