Java|如何正确地在遍历 List 时删除元素
source link: https://mazhuang.org/2024/04/29/java-list-remove-in-loop/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Java|如何正确地在遍历 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#remove
里 modCount++
,但并不会修改到 Itr 的 expectedModCount
——它们当然就不相等了。而 ArrayList$Itr#remove
在先调用了 ArrayList#remove
后,又将 modCount
的最新值赋给了 modCount
,这样就保证了 modCount
和 expectedModCount
的一致性。
同时,ArrayList$Itr#remove
里还有一个 cursor = lastRet
,实际上是将迭代器的游标做了修正,前移一位,以实现后续调用 next()
的行为正确。
源码面前,了无秘密。
- 如果需要在遍历 List 时删除元素,应使用迭代器的写法,即
iterator.remove()
; - 在非遍历场景下,使用
ArrayList#remove
也没什么问题——同理,即使是遍历场景下,使用ArrayList#remove
后马上 break 也 OK; - 如果遍历时做的事情不多,
Collection#removeIf
方法也是一个不错的选择(实际也是上述迭代器写法的封装)。
Recommend
-
10
遍历聚合对象中的元素——迭代器模式(六) 6 迭代器模式总结
-
9
5 JDK内置迭代器 为了让开发人员能够更加方便地操作聚合对象,在Java、C#等编程语言中都提供了内置迭代器。在Java集合框架中,常用的List和Set等聚合类都继承(或实现)了java.util.Collection接口,在Collection接口中声明了如下方法(部分)...
-
9
4 使用内部类实现迭代器 在迭代器模式结构图中,我们可以看到具体迭代器类和具体聚合类之间存在双重关系,其中一个关系为关联关系,在具体迭代器中需要维持一个对具体聚合对象的引用,该关联关系的目的是访问存储在聚合对象中的数据,以便迭代...
-
8
3 完整解决方案 为了简化AbstractObjectList类的结构,并给不同的具体数据集合类提供不同的遍历方式,Sunny软件公司开发人员使用迭代器模式来重构AbstractObjectList类的设计,重构之后的销售管理系统数据遍历结构如图4所示:
-
8
2 迭代器模式概述 在软件开发中,我们经常需要使用聚合对象来存储一系列数据。聚合对象拥有两个职责:一是存储数据;二是遍历数据。从依赖性来看,前者是聚合对象的基本职责;而后者既是可变化的,又是可分...
-
10
遍历聚合对象中的元素——迭代器模式(一) 20世纪80年代,...
-
6
golang slice 遍历删除 小风吹的我乱了 · 2018-11-13 22:34:39 · 13855 次点击 · 预计阅读时间不到 1 分钟 · 大约8小时之前 开始浏览
-
17
V2EX › 程序员 Java 遍历 list<map> 实现 left join 快,还是 MySQL left join 速度快
-
2
HashSet 类图
-
4
.NET性能优化-快速遍历List集合 Sys...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK