1. Vấn đề ConcurrentModificationException
Trong bài này sẽ (gần như) không có gì mới. Nhưng nó rất quan trọng, vì xóa dữ liệu không cẩn thận là một trong những lỗi khó khắc phục nhất — đặc biệt là trên production. Và vẫn sẽ có một lớp thú vị mà có lẽ bạn sẽ thích!
Vì vậy, một lần nữa và thêm lần nữa: tuyệt đối không dùng for-each để xóa! Dù bạn có yêu thích nó thế nào đi nữa.
Hãy bắt đầu bằng một ví dụ kinh điển gây đau đầu cho nhiều người mới (và không chỉ người mới):
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
// Thử xóa tất cả các số chẵn
for (Integer n : numbers) {
if (n % 2 == 0) {
numbers.remove(n); // BOOM! ConcurrentModificationException
}
}
Trông có vẻ như mọi thứ sẽ chạy, nhưng thực tế chương trình ném ngoại lệ:
Exception in thread "main" java.util.ConcurrentModificationException
Bây giờ hãy xem kỹ chuyện gì đã xảy ra. Khi bạn duyệt collection bằng for-each (hoặc Iterator thông thường), bên trong collection có duy trì một “bộ đếm thay đổi”. Nếu trong lúc duyệt, collection bị thay đổi không thông qua chính iterator, bộ đếm đó sẽ phát hiện “can thiệp từ bên ngoài” và ném ngoại lệ. Đây là cơ chế bảo vệ để chương trình không làm việc với cấu trúc dữ liệu bị hỏng.
2. Sử dụng Iterator
Xóa phần tử đúng cách trong khi duyệt như thế nào?
Nhắc lại: Iterator là đối tượng chuyên biệt cho phép duyệt collection và xóa an toàn các phần tử “ngay tại chỗ”. Nó giống như một bồi bàn vừa mang món ra vừa có thể dọn đĩa trong lúc đi vòng quanh bàn.
Lấy iterator
Iterator<Integer> it = numbers.iterator();
Duyệt bằng while và xóa qua it.remove()
Đây là cách đúng để xóa tất cả số chẵn khỏi danh sách:
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(); // Xóa an toàn phần tử hiện tại
}
}
System.out.println(numbers); // [1, 3, 5]
Điểm quan trọng: chỉ được xóa thông qua chính iterator (it.remove()) và chỉ sau khi gọi it.next(). Nếu cố gọi remove() hai lần liên tiếp mà không có next(), bạn sẽ nhận IllegalStateException.
3. ListIterator: tính năng mở rộng
Đây là “nhân vật mới” đã nhắc ở đầu bài! ListIterator là iterator “nâng cấp” dành cho danh sách (List), cho phép không chỉ xóa mà còn thêm phần tử trong khi duyệt, và di chuyển theo cả hai chiều (tiến và lùi).
Khác gì so với Iterator thường?
- Iterator — tuyến tính và cứng nhắc: chỉ tiến, chỉ xóa.
- ListIterator — linh hoạt và cơ động: tiến và lùi, xóa, thêm qua add(), và còn có thể thay thế phần tử hiện tại bằng set().
Ví dụ: xóa và thêm phần tử
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(); // Xóa các từ có 3 ký tự
it.add("pet"); // Đồng thời thêm "pet" ngay sau từ vừa xóa
}
}
System.out.println(words); // [pet, pet, bird]
Lưu ý: thêm qua it.add() sẽ chèn phần tử ngay sau vị trí hiện tại của iterator.
4. Xóa bằng removeIf
Từ Java 8, xuất hiện phương thức ngắn gọn và tiện lợi removeIf. Nó nhận lambda (hoặc bất kỳ Predicate nào) và xóa mọi phần tử mà điều kiện trả về true.
Ví dụ: xóa tất cả số chẵn
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]
Không chỉ ngắn gọn mà còn an toàn: bên trong, phương thức dùng iterator phù hợp — sẽ không có ConcurrentModificationException.
Ví dụ: xóa chuỗi ngắn hơn 3 ký tự
List<String> words = new ArrayList<>(List.of("hi", "cat", "no", "elephant"));
words.removeIf(word -> word.length() < 3);
System.out.println(words); // [cat, elephant]
Mẹo: nếu bạn chỉ cần xóa phần tử theo điều kiện — hãy dùng removeIf. Đây là cách ngắn gọn và hiện đại nhất.
5. Khuyến nghị thực hành
Nên ưu tiên cách nào?
- Nếu cần xóa theo điều kiện phức tạp và bạn dùng Java 8+: hãy dùng removeIf — ngắn gọn, dễ hiểu, an toàn.
- Nếu đang ở phiên bản Java cũ hơn hoặc cần logic duyệt phức tạp hơn: hãy dùng Iterator và phương thức remove() của nó.
- Nếu làm việc với List và muốn vừa xóa vừa thêm phần tử trong khi duyệt: hãy dùng ListIterator.
Đặc thù theo từng loại collection
- List: hỗ trợ tất cả các cách đã mô tả (Iterator, ListIterator, removeIf).
- Set: không có chỉ mục, nhưng Iterator chuẩn và removeIf vẫn hoạt động.
- Map: để xóa theo điều kiện, dùng iterator trên entrySet():
Với Java 8+ mọi thứ đơn giản hơn nhiều: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. Ví dụ thực tế: lọc người dùng
Giả sử có danh sách người dùng và chúng ta muốn xóa tất cả người dùng dưới 18 tuổi.
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)
));
// Xóa người chưa đủ 18 bằng removeIf
users.removeIf(user -> user.age < 18);
System.out.println(users); // [Boris (20), Gleb (25)]
7. So sánh các cách
Hãy lập một bảng nhỏ để củng cố; bộ não của chúng ta thích điều đó.
| Cách | Hỗ trợ từ phiên bản | Ngắn gọn | An toàn | Linh hoạt |
|---|---|---|---|---|
|
Java 5+ | - | ❌ | - |
|
Java 5+ | + | ✅ | + |
|
Java 5+ | + | ✅ | ++ |
|
Java 8+ | ++ | ✅ | + |
8. Các lỗi thường gặp khi xóa phần tử khỏi collection
Lỗi số 1: cố gắng xóa phần tử trong for-each
for (String s : list) {
if (s.equals("test")) {
list.remove(s);
}
}
Bạn đã biết: không được làm vậy — sẽ gặp ConcurrentModificationException! Hãy dùng iterator hoặc removeIf.
Lỗi số 2: gọi remove() trên iterator mà không gọi next()
Iterator<String> it = list.iterator();
it.remove(); // IllegalStateException — không thể xóa trước khi gọi next()
Lỗi số 3: cố xóa phần tử khỏi collection không thể thay đổi
List<String> immutable = List.of("a", "b", "c");
immutable.removeIf(s -> s.equals("a")); // UnsupportedOperationException
Các phương thức xóa không được hỗ trợ với collection bất biến.
Lỗi số 4: cố xóa phần tử khỏi Map thông qua values() hoặc keySet() mà không dùng iterator
for (String key : map.keySet()) {
if (key.startsWith("a")) {
map.remove(key); // ConcurrentModificationException!
}
}
Hãy dùng iterator trên entrySet() hoặc removeIf.
GO TO FULL VERSION