В чем причина возникновения ConcurrentModificationException в Java коллекциях?
Что проверяют
Понимаете ли вы, что ConcurrentModificationException чаще всего возникает в одном потоке, а не из-за конкуренции, и знаете ли механизм fail-fast. Следом просят показать, как правильно удалять элементы при обходе и почему CopyOnWriteArrayList подходит не всегда.
Как отвечать
Механизм такой. У ArrayList, HashMap и других коллекций из java.util есть счётчик структурных изменений modCount. Итератор при создании запоминает его и на каждом next() сверяет с текущим. Если коллекцию структурно изменили в обход итератора (добавили или удалили элемент, а не заменили значение через set), итератор бросает исключение.
Типичные причины:
- удаление в for-each:
for (String s : list) { if (...) list.remove(s); }, ведь for-each под капотом работает через итератор; - изменение коллекции внутри
forEachили стрима, который обходит эту же коллекцию; - другой поток меняет несинхронизированную коллекцию во время обхода. Здесь исключение не гарантировано: документация называет fail-fast поведением без гарантий, и данные могут испортиться молча.
Как исправить:
Iterator.remove(), а для списков ещёListIterator.add()иset()при ручном обходе;Collection.removeIf(predicate): самый короткий вариант, есть с Java 8;- сбор изменений в отдельный список с применением после обхода или новый список через стрим с
filter; - многопоточность:
ConcurrentHashMapсо слабо согласованными итераторами иCopyOnWriteArrayList, итератор которого обходит снимок массива. УCollections.synchronizedListобход всё равно нужно оборачивать вsynchronizedвручную.
Где спотыкаются
Называют причиной только многопоточность и лечат ошибку synchronized, хотя она возникла в одном потоке.
Уверенно говорят, что любое удаление в цикле бросает исключение. Удаление предпоследнего элемента проходит молча: размер уменьшается, hasNext() возвращает false, и последний элемент просто не обходится. Этот случай любят давать задачей.
Советуют CopyOnWriteArrayList на всё. Он копирует массив на каждую запись, годится для редко меняющихся списков, а remove() у его итератора бросает UnsupportedOperationException.