Java|如何正确地在遍历 List 时删除元素

简介: 从源码分析如何正确地在遍历 List 时删除元素。为什么有的写法会导致异常,而另一些不会。

最近在一个 Android 项目里遇到一个偶现的 java.util.ConcurrentModificationException 异常导致的崩溃,经过排查,导致异常的代码大概是这样的:

private List<XxxListener> listeners;

public void foo() {
    for (XxxListener listener : listeners) {
        listener.doSomething();
    }
}

public class XxxListener {
    public void doSomething() {
        // some code here
        if (...) {
            listeners.remove(this);
        }
    }
}

把函数调用展开一下就等效于:

for (XxxListener listener : listeners) {
    // some code here
    if (...) {
        listeners.remove(listener);
    }
}

这个异常之所以不是必现,是因为 listeners.remove 不是总被执行到。

我先直接说一下正确的写法吧,就是使用迭代器的写法:

Iterator<XxxListener> iterator = listeners.iterator();
while (iterator.hasNext()) {
    XxxListener listener = iterator.next();
    // some code here
    if (...) {
        iterator.remove();
    }
}

然后再进一步分析。

源码分析

先来从源码层面分析下上述 java.util.ConcurrentModificationException 异常是如何抛出的。

写一段简单的测试源码:

List<String> list = new ArrayList<>();
list.add("Hello");
list.add("World");
list.add("Hi");

for (String str : list) {
    list.remove(str);
}

执行抛出异常:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
    at java.util.ArrayList$Itr.next(ArrayList.java:861)

由此可以推测,for (String str : list) 这种写法实际只是一个语法糖,编译器会将其转换为迭代器的写法:

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String str = iterator.next();
    // do something
}

这可以从反编译后的字节码得到验证:

36: invokeinterface #8,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
41: astore_2
42: aload_2
43: invokeinterface #9,  1            // InterfaceMethod java/util/Iterator.hasNext:()Z
48: ifeq          72
51: aload_2
52: invokeinterface #10,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
57: checkcast     #11                 // class java/lang/String
60: astore_3
61: aload_1
62: aload_3
63: invokeinterface #12,  2           // InterfaceMethod java/util/List.remove:(Ljava/lang/Object;)Z

那么,iterator.next() 里发生了什么导致了异常的抛出呢?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
    int expectedModCount = modCount;

    public E next() {
        checkForComodification();
        // ...
    }

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

    // ...
 }

其中 modCount 是 ArrayList 类的成员,表示对 ArrayList 进行增删改的次数。expectedModCount 是 ArrayList$Itr 类的成员,初始值是迭代器创建时 ArrayList 的 modCount 的值。在每次调用 next() 时,都会检查 modCount 是否等于 expectedModCount,如果不等则抛出异常。

那为什么 list.remove 会导致 modCount 的值不等于 expectedModCount,而 iterator.remove 不会呢?

// 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;
}

// 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();
    }
}

可以看到 ArrayList#removemodCount++,但并不会修改到 Itr 的 expectedModCount——它们当然就不相等了。而 ArrayList$Itr#remove 在先调用了 ArrayList#remove 后,又将 modCount 的最新值赋给了 modCount,这样就保证了 modCountexpectedModCount 的一致性。

同时,ArrayList$Itr#remove 里还有一个 cursor = lastRet,实际上是将迭代器的游标做了修正,前移一位,以实现后续调用 next() 的行为正确。

小结

源码面前,了无秘密。

  • 如果需要在遍历 List 时删除元素,应使用迭代器的写法,即 iterator.remove()

  • 在非遍历场景下,使用 ArrayList#remove 也没什么问题——同理,即使是遍历场景下,使用 ArrayList#remove 后马上 break 也 OK;

  • 如果遍历时做的事情不多,Collection#removeIf 方法也是一个不错的选择(实际也是上述迭代器写法的封装)。


如果读完文章有收获,可以关注我的微信公众号「闷骚的程序员」并🌟设为星标🌟,随时阅读更多内容。

目录
相关文章
|
12天前
|
Java 机器人 程序员
从入门到精通:五种 List 遍历方法对比与实战指南
小米是一位热爱分享技术的程序员,本文详细介绍了 Java 中遍历 List 的五种方式:经典 for 循环、增强 for 循环、Iterator 和 ListIterator、Stream API 以及 forEach 方法。每种方式都有其适用场景和优缺点,例如 for 循环适合频繁访问索引,增强 for 循环和 forEach 方法代码简洁,Stream API 适合大数据量操作,ListIterator 支持双向遍历。文章通过生动的小故事和代码示例,帮助读者更好地理解和选择合适的遍历方式。
30 2
|
1月前
|
存储 Java 开发者
在 Java 中,如何遍历一个 Set 集合?
【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。
|
1月前
|
Java 开发者
|
2月前
|
安全 Java 程序员
深入Java集合框架:解密List的Fail-Fast与Fail-Safe机制
本文介绍了 Java 中 List 的遍历和删除操作,重点讨论了快速失败(fail-fast)和安全失败(fail-safe)机制。通过普通 for 循环、迭代器和 foreach 循环的对比,详细解释了各种方法的优缺点及适用场景,特别是在多线程环境下的表现。最后推荐了适合高并发场景的 fail-safe 容器,如 CopyOnWriteArrayList 和 ConcurrentHashMap。
69 5
|
2月前
|
Java 程序员
Java|List.subList 踩坑小记
不应该仅凭印象和猜测,就开始使用一个方法,至少花一分钟认真读完它的官方注释文档。
29 1
|
2月前
|
前端开发 小程序 Java
java基础:map遍历使用;java使用 Patten 和Matches 进行正则匹配;后端传到前端展示图片三种情况,并保存到手机
这篇文章介绍了Java中Map的遍历方法、使用Pattern和matches进行正则表达式匹配,以及后端向前端传输图片并保存到手机的三种情况。
27 1
|
2月前
|
存储 算法 Java
Java一分钟之-数组的创建与遍历
数组作为Java中存储和操作一组相同类型数据的基本结构,其创建和遍历是编程基础中的基础。通过不同的创建方式,可以根据实际需求灵活地初始化数组。而选择合适的遍历方法,则可以提高代码的可读性和效率。掌握这些基本技能,对于深入学习Java乃至其他编程语言的数据结构和算法都是至关重要的。
33 6
|
6月前
|
缓存 Java 测试技术
探讨Java中遍历Map集合的最快方式
探讨Java中遍历Map集合的最快方式
95 1
|
6月前
|
存储 缓存 Java
Java遍历Map集合的方法
在Java中,遍历Map集合主要有四种方式:1) 使用`keySet()`遍历keys并用`get()`获取values;2) 使用`entrySet()`直接遍历键值对,效率较高;3) 通过`Iterator`遍历,适合在遍历中删除元素;4) Java 8及以上版本可用`forEach`和Lambda表达式,简洁易读。`entrySet()`通常性能最佳,而遍历方式的选择应考虑代码可读性和数据量。
73 0
Java 遍历Map集合的各种姿势
最常用,在键值都需要时使用。 Map map = new HashMap(); for (Map.Entry entry : map.entrySet()) { System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue()); } 在for-each循环中遍历keys或values。
702 0