Java 21 模式匹配 for-each 遍历 ArrayList 与 ConcurrentModificationException 深度剖析
大家好,今天我们来深入探讨一个在 Java 开发中经常遇到的问题:在使用 Java 21 的模式匹配 for-each 循环遍历 ArrayList 时,可能会触发 ConcurrentModificationException 异常。我们将分析这种异常的原因,ArrayList 的 fail-fast 机制,模式匹配对这个过程的影响,以及如何避免这个异常。
ConcurrentModificationException:并发修改异常
ConcurrentModificationException 是 Java 集合框架中的一个运行时异常,通常发生在多线程环境下,但即使在单线程环境中,不正确地使用迭代器也会导致此异常。 它的根本原因是:当一个线程正在使用迭代器遍历集合时,另一个线程(或者同一个线程在不通过迭代器的情况下)修改了集合的结构,导致迭代器状态与集合实际状态不一致。
什么是集合的结构性修改?
结构性修改指的是任何改变集合元素数量的操作,例如:
- 添加元素
- 删除元素
- 清空集合
简单地修改集合中现有元素的值 通常 不被认为是结构性修改,但某些情况下也可能触发异常,这取决于集合的具体实现。
ArrayList 的 Fail-Fast 机制
ArrayList 实现了 fail-fast 机制。这意味着当 ArrayList 的迭代器检测到集合在迭代过程中被修改时,会立即抛出 ConcurrentModificationException。这样做是为了尽早发现错误,防止迭代器返回错误的结果,保证程序的正确性。
ArrayList 的 fail-fast 机制的实现依赖于一个名为 modCount 的成员变量。modCount 记录了 ArrayList 结构被修改的次数。
- 初始化: 当创建一个
ArrayList的迭代器时,迭代器会记录下当前ArrayList的modCount值。 - 迭代: 在迭代器的
next()、hasNext()、remove()等方法中,迭代器会检查当前的modCount值是否与迭代器创建时记录的modCount值相等。 - 检测到修改: 如果两者不相等,说明
ArrayList在迭代过程中被修改了,迭代器会抛出ConcurrentModificationException。
代码示例:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ConcurrentModificationExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
try {
while (iterator.hasNext()) {
String element = iterator.next();
if (element.equals("B")) {
list.remove("B"); // 直接修改 ArrayList
}
}
} catch (ConcurrentModificationException e) {
System.err.println("Caught ConcurrentModificationException: " + e.getMessage());
}
System.out.println("List after attempted modification: " + list);
}
}
在这个例子中,我们使用 iterator 遍历 list,但是在遍历过程中,我们直接使用 list.remove("B") 修改了 list 的结构。这会导致 iterator 检测到 modCount 发生了变化,从而抛出 ConcurrentModificationException。
正确的修改方式:使用 Iterator.remove()
为了避免 ConcurrentModificationException,应该使用迭代器提供的 remove() 方法来删除元素。Iterator.remove() 方法会在删除元素的同时更新迭代器的状态,保持 modCount 的一致性。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ConcurrentModificationExampleCorrected {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if (element.equals("B")) {
iterator.remove(); // 使用 iterator.remove()
}
}
System.out.println("List after modification: " + list);
}
}
在这个修改后的例子中,我们使用 iterator.remove() 来删除元素 "B"。这样就不会触发 ConcurrentModificationException。
Java 21 模式匹配 for-each 循环
Java 21 引入了模式匹配 for-each 循环,允许在循环中直接对集合元素进行模式匹配。 尽管语法糖让代码看起来更简洁,但是底层仍然是iterator的实现,因此也需要注意ConcurrentModificationException问题。
基本语法:
List<Object> objects = List.of("Hello", 123, "World", 456);
for (Object o : objects) {
switch (o) {
case String s -> System.out.println("String: " + s);
case Integer i -> System.out.println("Integer: " + i);
default -> System.out.println("Other: " + o);
}
}
在这个例子中,我们使用模式匹配 for-each 循环遍历 objects 列表,并根据元素的类型执行不同的操作。
模式匹配 for-each 与 ConcurrentModificationException
模式匹配 for-each 循环本质上是使用迭代器遍历集合的语法糖。因此,如果在模式匹配 for-each 循环中直接修改集合的结构,同样会触发 ConcurrentModificationException。
代码示例:
import java.util.ArrayList;
import java.util.List;
public class PatternMatchingForEachConcurrentModification {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
list.add("A");
list.add(1);
list.add("B");
list.add(2);
try {
for (Object o : list) {
switch (o) {
case String s -> {
if (s.equals("B")) {
list.remove("B"); // 直接修改 ArrayList
}
System.out.println("String: " + s);
}
case Integer i -> System.out.println("Integer: " + i);
default -> System.out.println("Other: " + o);
}
}
} catch (ConcurrentModificationException e) {
System.err.println("Caught ConcurrentModificationException: " + e.getMessage());
}
System.out.println("List after attempted modification: " + list);
}
}
在这个例子中,我们在模式匹配 for-each 循环中直接使用 list.remove("B") 修改了 list 的结构。这会导致迭代器检测到 modCount 发生了变化,从而抛出 ConcurrentModificationException。
如何避免 ConcurrentModificationException?
由于模式匹配 for-each 循环无法直接访问迭代器,因此无法使用 Iterator.remove() 方法。 以下是几种解决方案:
-
使用
removeIf()方法: Java 8 引入了removeIf()方法,可以根据条件删除集合中的元素。removeIf()方法会在内部处理迭代器的状态,避免ConcurrentModificationException。import java.util.ArrayList; import java.util.List; public class PatternMatchingForEachRemoveIf { public static void main(String[] args) { List<Object> list = new ArrayList<>(); list.add("A"); list.add(1); list.add("B"); list.add(2); list.removeIf(o -> { if (o instanceof String s && s.equals("B")) { return true; // 删除满足条件的元素 } return false; }); for (Object o : list) { switch (o) { case String s -> System.out.println("String: " + s); case Integer i -> System.out.println("Integer: " + i); default -> System.out.println("Other: " + o); } } System.out.println("List after modification: " + list); } }在这个例子中,我们使用
removeIf()方法删除字符串 "B"。这样就不会触发ConcurrentModificationException。 -
创建新的集合: 如果需要对集合进行大量的修改,可以创建一个新的集合,将不需要删除的元素添加到新的集合中。
import java.util.ArrayList; import java.util.List; public class PatternMatchingForEachNewList { public static void main(String[] args) { List<Object> list = new ArrayList<>(); list.add("A"); list.add(1); list.add("B"); list.add(2); List<Object> newList = new ArrayList<>(); for (Object o : list) { switch (o) { case String s -> { if (!s.equals("B")) { newList.add(s); } } case Integer i -> newList.add(i); default -> newList.add(o); } } for (Object o : newList) { switch (o) { case String s -> System.out.println("String: " + s); case Integer i -> System.out.println("Integer: "i); default -> System.out.println("Other: " + o); } } System.out.println("List after modification: " + newList); } }在这个例子中,我们创建了一个新的
newList,将不需要删除的元素添加到newList中。这样就不会触发ConcurrentModificationException。 -
使用
CopyOnWriteArrayList:CopyOnWriteArrayList是java.util.concurrent包中的一个线程安全的ArrayList实现。 它的特点是:每次修改集合时,都会创建一个新的副本。 这意味着迭代器始终在原始集合的副本上进行迭代,因此不会受到其他线程的修改的影响。import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; public class PatternMatchingForEachCopyOnWriteArrayList { public static void main(String[] args) { List<Object> list = new CopyOnWriteArrayList<>(); list.add("A"); list.add(1); list.add("B"); list.add(2); for (Object o : list) { switch (o) { case String s -> { if (s.equals("B")) { list.remove("B"); // 安全地修改 CopyOnWriteArrayList } System.out.println("String: " + s); } case Integer i -> System.out.println("Integer: " + i); default -> System.out.println("Other: " + o); } } System.out.println("List after modification: " + list); } }在这个例子中,我们使用
CopyOnWriteArrayList,可以在迭代过程中安全地修改集合。 但是,需要注意的是,CopyOnWriteArrayList的性能比较差,因为每次修改都需要创建一个新的副本。 因此,只有在并发修改的情况下才应该使用CopyOnWriteArrayList。 -
使用迭代器手动遍历: 显式地使用
Iterator对象,并调用iterator.remove()方法。 这种方式最为灵活,但代码也相对冗长。import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class PatternMatchingForEachIteratorRemove { public static void main(String[] args) { List<Object> list = new ArrayList<>(); list.add("A"); list.add(1); list.add("B"); list.add(2); Iterator<Object> iterator = list.iterator(); while (iterator.hasNext()) { Object o = iterator.next(); switch (o) { case String s -> { if (s.equals("B")) { iterator.remove(); // 使用迭代器删除 } else { System.out.println("String: " + s); } } case Integer i -> System.out.println("Integer: " + i); default -> System.out.println("Other: " + o); } } System.out.println("List after modification: " + list); } }
总结:
| 解决方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
removeIf() |
简洁,易于使用,避免 ConcurrentModificationException |
只能根据条件删除元素 | 只需要删除满足特定条件的元素,且不需要对集合进行大量的修改 |
| 创建新的集合 | 可以对集合进行大量的修改 | 需要额外的内存空间,可能导致性能问题 | 需要对集合进行大量的修改,且对性能要求不高 |
CopyOnWriteArrayList |
线程安全,可以在并发修改的情况下安全地修改集合 | 性能较差,每次修改都需要创建一个新的副本 | 多线程环境,需要在迭代过程中修改集合,且对性能要求不高 |
| 使用Iterator手动遍历 | 灵活,可以控制删除的逻辑 | 代码相对冗长 | 需要精确控制删除逻辑的场景 |
模式匹配与Switch语句
模式匹配不仅仅局限于for-each循环,它在switch语句中也得到了广泛应用。 结合switch语句使用模式匹配时,同样需要注意修改集合带来的问题。
import java.util.ArrayList;
import java.util.List;
public class SwitchPatternMatchingConcurrentModification {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
list.add("A");
list.add(1);
list.add("B");
list.add(2);
try {
for (int i = 0; i < list.size(); i++) {
Object o = list.get(i);
switch (o) {
case String s when s.equals("B") -> {
list.remove(i); // 直接修改 ArrayList
System.out.println("Removed String: " + s);
i--; // 调整索引,避免跳过元素
}
case String s -> System.out.println("String: " + s);
case Integer j -> System.out.println("Integer: " + j);
default -> System.out.println("Other: " + o);
}
}
} catch (ConcurrentModificationException e) {
System.err.println("Caught ConcurrentModificationException: " + e.getMessage());
} catch (IndexOutOfBoundsException e){
System.err.println("Caught IndexOutOfBoundsException: " + e.getMessage());
}
System.out.println("List after attempted modification: " + list);
}
}
在这个例子中,我们使用传统的for循环和switch语句结合模式匹配。 当遇到字符串"B"时,我们直接使用list.remove(i)删除元素。 尽管我们尝试通过i--调整索引,以避免跳过元素,但这种做法仍然可能导致问题,例如IndexOutOfBoundsException或逻辑错误,并且仍然不推荐,因为它违背了fail-fast的设计原则。 更好的做法是避免在遍历过程中修改集合,或者使用上述提到的removeIf()或创建新集合的方法。
总结
- ConcurrentModificationException发生在迭代过程中修改集合结构时。
- ArrayList的fail-fast机制通过modCount来检测并发修改。
- 模式匹配for-each本质上是iterator的语法糖,直接修改集合会导致ConcurrentModificationException。
- 可以使用removeIf()、创建新集合、CopyOnWriteArrayList或迭代器手动遍历来避免ConcurrentModificationException。
希望今天的讲解能够帮助大家更好地理解 Java 21 模式匹配 for-each 循环与 ConcurrentModificationException 的关系,并在实际开发中避免此类问题。谢谢大家。