首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java ArrayList 遍歷與刪除正確姿勢:避開並發修改異常陷阱

     編輯:Java 相關知識

在 Java 開發中,遍歷集合時刪除元素是高頻操作,但稍有不慎即拋出 ConcurrentModificationException。本文通過完整示例代碼,演示正確的遍歷刪除姿勢,並對比多種方案優劣,助你徹底掌握這一核心技能。

問題重現:為什麼會拋異常?

List list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
for (String s : list) {           // 增強 for 底層使用 Iterator
    if ("B".equals(s)) {
        list.remove(s);           // ❌ 直接操作集合,modCount 不一致
    }
}
// 拋出 ConcurrentModificationException

根因:增強 for / 普通 Iterator 遍歷時,內部維護 `expectedModCount`;直接調用 `List.remove()` 修改 `modCount` 導致不匹配,觸發 fail-fast 機制。

正確姿勢對比(5 種方案)

方案一:Iterator.remove() —— 標準、通用、推薦

List list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
Iterator it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if ("B".equals(s)) {
        it.remove();              // ✅ 同步更新 expectedModCount
    }
}
// 結果:[A, C, D]
  • 優點:JDK 1.2+ 支援、所有 List 實現通用、O(1) 刪除(ArrayList 尾部)、語義清晰。
  • 缺點:代碼稍繁瑣。

方案二:ListIterator(需雙向/索引操作時)

ListIterator lit = list.listIterator();
while (lit.hasNext()) {
    if ("B".equals(lit.next())) {
        lit.remove();             // ✅ 同理安全
    }
}
  • 優點:支援 `previous()`、`nextIndex()`、`set()` 等雙向操作。

方案三:倒序索引遍歷(ArrayList 專用、性能最優)

for (int i = list.size() - 1; i >= 0; i--) {
    if ("B".equals(list.get(i))) {
        list.remove(i);           // ✅ 從後向前,索引不受影響
    }
}
  • 優點:無 Iterator 開銷、隨機訪問 O(1)、ArrayList 尾部刪除無數組拷貝、性能最佳。
  • 缺點:僅適用 ArrayList 等 RandomAccess 列表,LinkedList 會退化為 O(n²)。

方案四:Java 8+ removeIf() —— 一行代碼、內部用 Iterator

list.removeIf(s -> "B".equals(s));  // ✅ 內部實現同方案一
// 結果:[A, C, D]
  • 優點:極度簡潔、函數式風格、默認方法實現高效。
  • 缺點:無法在刪除時執行額外邏輯(如日誌、統計)。

方案五:Stream 過濾收集新列表(不可變風格、並行友好)

List filtered = list.stream()
                              .filter(s -> !"B".equals(s))
                              .collect(Collectors.toList());
// 原列表不變,生成新列表
  • 優點:無副作用、線程安全、易並行(`parallelStream()`)、鏈式操作靈活。
  • 缺點:額外內存分配、非原地修改。

完整可運行示例

package com.example.collection;

import java.util.*;

public class ArrayListTraverseRemoveDemo {

    public static void main(String[] args) {
        // 準備數據
        List students = new ArrayList<>();
        students.add(new Student("小紅", 13, "白色"));
        students.add(new Student("小明", 12, "黃色"));
        students.add(new Student("小強", 123, "藍色"));
        students.add(new Student("小李", 23, "白色"));
        students.add(new Student("小愛", 4, "blue"));
        students.add(new Student("小北", 2, "brave"));

        System.out.println("=== 原始列表 ===");
        printList(students);

        // 方案一:Iterator.remove() 刪除姓名含"小"的學生(演示)
        Iterator it = students.iterator();
        while (it.hasNext()) {
            Student s = it.next();
            if (s.getName().contains("小")) {  // 全部命中,僅演示
                it.remove();
            }
        }
        System.out.println("=== Iterator.remove() 後 ===");
        printList(students);  // 空列表

        // 重置數據演示方案三:倒序索引刪除學號 > 100
        students.addAll(Arrays.asList(
            new Student("小紅", 13, "白色"),
            new Student("小明", 12, "黃色"),
            new Student("小強", 123, "藍色"),
            new Student("小李", 23, "白色")
        ));
        for (int i = students.size() - 1; i >= 0; i--) {
            if (students.get(i).getNum() > 100) {
                students.remove(i);
            }
        }
        System.out.println("=== 倒序索引刪除 num>100 後 ===");
        printList(students);

        // 方案四:removeIf 刪除顏色為"白色"
        students.removeIf(s -> "白色".equals(s.getColor()));
        System.out.println("=== removeIf 刪除白色後 ===");
        printList(students);
    }

    static void printList(List list) {
        System.out.println("大小:" + list.size());
        for (int i = 0; i < list.size(); i++) {
            Student s = list.get(i);
            System.out.printf("第%d個:姓名=%s, 學號=%d, 顏色=%s%n",
                    i + 1, s.getName(), s.getNum(), s.getColor());
        }
        System.out.println("------------");
    }

    static class Student {
        private String name;
        private int num;
        private String color;

        public Student(String name, int num, String color) {
            this.name = name;
            this.num = num;
            this.color = color;
        }
        public String getName() { return name; }
        public int getNum() { return num; }
        public String getColor() { return color; }
    }
}

輸出結果

=== 原始列表 ===
大小:6
第1個:姓名=小紅, 學號=13, 顏色=白色
第2個:姓名=小明, 學號=12, 顏色=黃色
第3個:姓名=小強, 學號=123, 顏色=藍色
第4個:姓名=小李, 學號=23, 顏色=白色
第5個:姓名=小愛, 學號=4, 顏色=blue
第6個:姓名=小北, 學號=2, 顏色=brave
------------
=== Iterator.remove() 後 ===
大小:0
------------
=== 倒序索引刪除 num>100 後 ===
大小:3
第1個:姓名=小紅, 學號=13, 顏色=白色
第2個:姓名=小明, 學號=12, 顏色=黃色
第3個:姓名=小李, 學號=23, 顏色=白色
------------
=== removeIf 刪除白色後 ===
大小:1
第1個:姓名=小明, 學號=12, 顏色=黃色
------------

選型決策表

場景首選方案備選避坑提醒
通用 List、需邊刪邊做額外邏輯Iterator.remove()ListIterator勿用增強 for 直接 remove
僅 ArrayList、單純刪除、追求極致性能倒序索引 forremoveIf()LinkedList 禁用
簡單條件刪除、代碼簡潔優先removeIf()Stream filter無法執行副作用邏輯
不可變風格、需保留原列表、並行處理Stream filter + collectList.copyOf + removeIf額外內存開銷
需雙向遍歷/修改/獲取索引ListIterator—同 Iterator 安全

核心原理回顧

  • fail-fast:`modCount` vs `expectedModCount`,非同步修改即拋異常,防止不確定行為。
  • Iterator.remove():同步更新 `expectedModCount = modCount`,打破檢測邏輯。
  • 倒序索引:從尾部刪除,前序元素索引不變,繞過 `modCount` 機制(但非標準用法,僅限 RandomAccess)。
  • removeIf/Stream:底層同 Iterator,語法糖簡化調用。

掌握這 5 種姿勢,根據場景靈活選擇,即可從容應對所有集合遍歷刪除需求,徹底告別 ConcurrentModificationException。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved