Питання 27 · Розділ 4

Що таке ConcurrentModificationException?

subList не створює новий список — це view на оригінал. SubList зберігає expectedModCount батька. Коли ви змінюєте батьківський список напряму, його modCount змінюється, а expect...

Мовні версії: English Russian Ukrainian

🟢 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

  1. CME = порушення контракту ітератора
  2. Best-effort → не гарантія у multi-threading
  3. Views (subList, keySet) → чутливі до modCount
  4. removeIf → оптимальний спосіб
  5. iterator.remove() → безпечний в циклі
  6. 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]]