Що таке ConcurrentModificationException?
subList не створює новий список — це view на оригінал. SubList зберігає expectedModCount батька. Коли ви змінюєте батьківський список напряму, його modCount змінюється, а expect...
🟢 Junior Level
CME (ConcurrentModificationException) — сигнал про те, що код порушив контракт ітератора: колекція була змінена НЕ через методи самого ітератора.
Важливо: CME — це НЕ помилка багатопотоковості (як можна подумати за назвою). Це пастка для відлову багів в одному потоці.
List<String> list = new ArrayList<>(List.of("A", "B", "C"));
// ❌ Помилка!
for (String s : list) {
list.remove(s); // → ConcurrentModificationException!
}
// ✅ Правильно:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
it.remove(); // OK!
}
Важливо: Найчастіше буває в одному потоці, а не в багатопотоковості!
🟡 Middle Level
Механіка: modCount
// Колекція: modCount (число змін)
// Ітератор: expectedModCount (копія при створенні)
// При next():
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
Підступні сценарії
SubList пастка:
List<String> sub = list.subList(0, 2);
list.add("X"); // Змінили батьківський
sub.size(); // → CME! SubList чутливий до modCount батька
subList не створює новий список — це view на оригінал. SubList зберігає expectedModCount батька. Коли ви змінюєте батьківський список напряму, його modCount змінюється, а expectedModCount в SubList — ні. Перевірка виявляє розсинхронізацію.
Останній елемент:
// Іноді CME НЕ вилітає при видаленні передостаннього!
// → hasNext() повертає false → цикл завершується
// → Логічна помилка не спіймана!
Якщо видалити передостанній елемент через list.remove(), hasNext() може вже повернути false (ітератор «думає», що елементів немає). Цикл завершується без перевірки modCount → CME не вилітає, але дані вже пошкоджені. Це не баг CME — це best-effort детектор.
Способи вирішення
// 1. Iterator.remove()
it.remove(); // Оновлює expectedModCount
// 2. removeIf (Java 8+)
list.removeIf(e -> condition(e));
// 3. Collect to remove
List<String> toRemove = new ArrayList<>();
for (String s : list) {
if (condition(s)) toRemove.add(s);
}
list.removeAll(toRemove);
// 4. Concurrent колекції
CopyOnWriteArrayList<String> safe = new CopyOnWriteArrayList<>(list);
// → Ітератор = snapshot, не кидає CME
🔴 Senior Level
CME ≠ багатопотоковість
CME найчастіше в ОДНОМУ потоці!
→ Видалення в for-each циклі
У багатопотоковості без синхронізації:
→ modCount не volatile
→ Потік може НЕ побачити зміну
→ ArrayIndexOutOfBoundsException замість CME!
Views чутливі до modCount
// keySet, entrySet, subList → view на оригінал
// Зміна оригіналу → CME в view!
Map<String, Integer> map = new HashMap<>();
Set<String> keys = map.keySet();
map.put("A", 1); // Змінили map
for (String k : keys) { ... } // → CME!
View (представлення) — об’єкт, який не зберігає свої дані, а показує дані оригінальної колекції. Зміни в оригіналі миттєво видні через view, і навпаки.
Best Practices
- CME = порушення контракту ітератора
- Best-effort → не гарантія у multi-threading
- Views (subList, keySet) → чутливі до modCount
- removeIf → оптимальний спосіб
- iterator.remove() → безпечний в циклі
- Concurrent колекції → weakly consistent, без CME
Резюме для Senior
- modCount = детектор змін
- Best-effort → не 100% у multi-threading
- Views → CME при зміні оригіналу
- removeIf > iterator.remove() > collect-and-remove
- Concurrent колекції → немає CME
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- CME виникає при зміні колекції НЕ через методи ітератора (найчастіше в одному потоці!)
- Механізм:
modCountколекції !=expectedModCountітератора при викликуnext() - CME — не помилка багатопотоковості, а пастка для відлову багів логіки
subList,keySet,entrySet— view-об’єкти, чутливі доmodCountбатька- При видаленні передостаннього елемента CME може НЕ вилетіти (best-effort детектор)
- 4 способи вирішення:
iterator.remove(),removeIf(), collect-and-remove, concurrent колекції iterator.remove()оновлюєexpectedModCount— CME не виникає
Часті уточнюючі запитання:
- Чому CME найчастіше в одному потоці, а не в багатопотоковості? — Найчастіший сценарій: видалення елемента в for-each циклі одним потоком.
- Чому subList кидає CME при зміні батька? — SubList зберігає
expectedModCountбатька; пряма зміна батька розсинхронізує. - Що буде в багатопотоковості без синхронізації замість CME? —
ArrayIndexOutOfBoundsException— потік не бачить змінуmodCountчерез відсутність volatile. - Чому CME може не вилетіти при видаленні передостаннього елемента? —
hasNext()може повернутиfalseдо перевіркиmodCount— цикл завершується без детекції.
Червоні прапорці (НЕ говорити):
- «CME — це помилка багатопотоковості» — ні, найчастіше виникає в одному потоці
- «CME гарантує виявлення змін у багатопотоковості» — ні, best-effort, modCount не volatile
- «subList створює незалежний список» — ні, це view на оригінал, чутливий до modCount
- «Завжди можна покластися на CME для синхронізації» — це детектор багів, не механізм синхронізації
Пов’язані теми:
- [[26. Що таке fail-fast та fail-safe ітератори]]
- [[28. Як правильно видаляти елементи під час ітерації]]
- [[25. В чому різниця між Iterator та ListIterator]]