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(). Якщо спробувати викликати remove() двічі поспіль без next(), отримаєте 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(); // Видаляємо слова з трьох літер
it.add("pet"); // Одразу додаємо "pet" після видаленого слова
}
}
System.out.println(words); // [pet, pet, bird]
Зауваження: додавання через it.add() вставляє елемент одразу після поточної позиції ітератора.
4. Видалення за допомогою removeIf
Починаючи з Java 8, з’явився лаконічний і зручний метод removeIf. Він приймає лямбда-вираз (або будь-який 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 не буде.
Приклад: видалення рядків коротших за три символи
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("Аня", 17),
new User("Борис", 20),
new User("Віка", 15),
new User("Гліб", 25)
));
// Видаляємо неповнолітніх через removeIf
users.removeIf(user -> user.age < 18);
System.out.println(users); // [Борис (20), Гліб (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: виклик remove() без попереднього next()
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: спроба видаляти елементи з Map через values() або keySet() без ітератора
for (String key : map.keySet()) {
if (key.startsWith("a")) {
map.remove(key); // ConcurrentModificationException!
}
}
Використовуйте ітератор над entrySet() або removeIf.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ