1. 關於 ConcurrentModificationException 的問題
這堂課幾乎沒有(幾乎)新內容。但它非常重要,因為不小心刪除資料屬於最難挽回的錯誤之一——尤其在生產環境。而且還會出現一個你很可能會喜歡的有趣類別!
所以,是的,再次強調:刪除時絕對不要用 for-each!不論你多麼喜歡它。
我們從一個經典的例子開始,這常讓許多新手(甚至老手)感到痛苦:
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
// 嘗試刪除所有偶數
for (Integer n : numbers) {
if (n % 2 == 0) {
numbers.remove(n); // 砰!ConcurrentModificationException
}
}
看起來一切都應該能運作,但實際上程式會丟出例外:
Exception in thread "main" java.util.ConcurrentModificationException
現在我們來詳細說明發生了什麼。當你透過 for-each(或一般的 Iterator)遍歷集合時,集合內部會維護一個特殊的「修改計數器」。如果在遍歷期間集合被不是透過同一個迭代器的方式修改,這個計數器會偵測到「外部干預」並丟出例外。這是防呆機制,避免程式在損壞的資料結構上運作。
2. 使用 Iterator
遍歷時如何正確刪除元素?
提醒一下:Iterator 是一種特殊物件,能讓你遍歷集合並在過程中安全地「當下」刪除元素。它就像服務生,不僅能端菜,還能在繞桌時順手收走盤子。
取得迭代器
Iterator<Integer> it = numbers.iterator();
用 while 遍歷並透過 it.remove() 刪除
以下是正確刪除清單中所有偶數的方法:
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
Integer n = it.next();
if (n % 2 == 0) {
it.remove(); // 安全地刪除目前元素
}
}
System.out.println(numbers); // [1, 3, 5]
重要:只能透過迭代器本身(it.remove())刪除,且必須在呼叫 it.next() 之後。如果沒有先呼叫 next() 就連續呼叫 remove() 兩次,會得到 IllegalStateException。
3. ListIterator:進階功能
這就是一開始預告的新內容!ListIterator 是針對列表(List)的「增強版」迭代器,不僅能刪除,還能在遍歷時新增元素,並可雙向移動(向前與向後)。
與一般 Iterator 的差異
- Iterator:直來直往——只能往前,只能刪除。
- ListIterator:更靈活——能前後移動;可刪除、可透過 add() 新增;還能用 set() 取代當前元素。
範例:刪除與新增元素
List<String> words = new ArrayList<>(List.of("cat", "dog", "bird"));
ListIterator<String> it = words.listIterator();
while (it.hasNext()) {
String word = it.next();
if (word.length() == 3) {
it.remove(); // 刪除長度為 3 的單字
it.add("pet"); // 立刻在被刪除的位置之後加入 "pet"
}
}
System.out.println(words); // [pet, pet, bird]
注意:透過 it.add() 新增,會把元素插在迭代器目前位置之後。
4. 使用 removeIf
自 Java 8 起,提供了簡潔好用的方法 removeIf。它接受 Lambda 表達式(或任何 Predicate),並刪除所有使條件回傳 true 的元素。
範例:刪除所有偶數
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
numbers.removeIf(n -> n % 2 == 0);
System.out.println(numbers); // [1, 3, 5]
這不僅更短,而且安全:在內部會使用正確的迭代器——不會出現 ConcurrentModificationException。
範例:刪除長度小於 3 的字串
List<String> words = new ArrayList<>(List.of("hi", "cat", "no", "elephant"));
words.removeIf(word -> word.length() < 3);
System.out.println(words); // [cat, elephant]
建議:如果只是要依條件刪除元素——請使用 removeIf。這是最精簡、現代的方式。
5. 實務建議
該優先選哪一種方式?
- 如果要按複雜條件刪除,且使用 Java 8+:請用 removeIf——簡潔、易讀且安全。
- 如果使用較舊的 Java 版本,或需要更複雜的遍歷邏輯:請使用 Iterator 與它的 remove() 方法。
- 如果處理的是 List,且希望在遍歷時不僅刪除還能新增元素:請使用 ListIterator。
不同集合類型的注意事項
- List:支援上述所有做法(Iterator、ListIterator、removeIf)。
- Set:沒有索引,但標準的 Iterator 與 removeIf 都適用。
- Map:若要依條件刪除,請對 entrySet() 使用迭代器:
而在 Java 8+ 之後,簡化許多:Map<String, Integer> map = new HashMap<>(Map.of("a", 1, "b", 2, "c", 3)); Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, Integer> entry = it.next(); if (entry.getValue() % 2 == 0) { it.remove(); } } System.out.println(map); // {a=1, c=3}map.entrySet().removeIf(entry -> entry.getValue() % 2 == 0);
6. 實務範例:篩選使用者
假設我們有一個使用者清單,想刪除所有未滿 18 歲的使用者。
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
List<User> users = new ArrayList<>(List.of(
new User("Anya", 17),
new User("Boris", 20),
new User("Vika", 15),
new User("Gleb", 25)
));
// 透過 removeIf 刪除未成年使用者
users.removeIf(user -> user.age < 18);
System.out.println(users); // [Boris (20), Gleb (25)]
7. 方法比較
用一張小表格來加深印象——我們的大腦就愛這種。
| 方式 | 自哪個版本支援 | 簡潔度 | 安全性 | 彈性 |
|---|---|---|---|---|
|
Java 5+ | - | ❌ | - |
|
Java 5+ | + | ✅ | + |
|
Java 5+ | + | ✅ | ++ |
|
Java 8+ | ++ | ✅ | + |
8. 在集合中刪除元素的常見錯誤
錯誤 1:在 for-each 中嘗試刪除元素
for (String s : list) {
if (s.equals("test")) {
list.remove(s);
}
}
你已經知道:不能這麼做——會得到 ConcurrentModificationException!請使用迭代器或 removeIf。
錯誤 2:在未呼叫 next() 前就對迭代器呼叫 remove()
Iterator<String> it = list.iterator();
it.remove(); // IllegalStateException — 在呼叫 next() 之前無法刪除
錯誤 3:試圖在不可變集合中刪除元素
List<String> immutable = List.of("a", "b", "c");
immutable.removeIf(s -> s.equals("a")); // UnsupportedOperationException
不可變集合不支援刪除相關的方法。
錯誤 4:透過 values() 或 keySet() 刪除 Map 中元素,但未使用迭代器
for (String key : map.keySet()) {
if (key.startsWith("a")) {
map.remove(key); // ConcurrentModificationException!
}
}
請使用對 entrySet() 的迭代器,或 removeIf。
GO TO FULL VERSION