ConcurrentModificationException при итерации ArrayList
Условие
Что произойдёт при выполнении следующего кода?
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {
if (s.equals("b")) {
list.add("d");
}
}
Вопросы
- Что произойдёт при выполнении?
- Почему возникает исключение?
- Что такое fail-fast итератор?
- Как правильно модифицировать коллекцию во время итерации?
Подсказка
Рассмотрите использование Iterator.remove(), CopyOnWriteArrayList или создание копии списка.
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
ConcurrentModificationException при итерации ArrayList
Ответ
Программа выбросит: ConcurrentModificationException
Почему возникает исключение?
Это происходит из-за механизма fail-fast итератора, встроенного в ArrayList. Когда вы изменяете размер списка во время итерации, итератор обнаруживает это и выбрасывает исключение.
Как это работает под капотом
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) { // for-each использует итератор
if (s.equals("b")) {
list.add("d"); // ОШИБКА! Модифицируем список напрямую
}
}
// java.util.ConcurrentModificationException
Внутреннее представление
// Примерно вот что происходит при for-each:
Iterator<String> iterator = list.iterator();
int modCount = list.modCount; // Сохраняем начальное значение
while (iterator.hasNext()) {
String s = iterator.next(); // Проверяет: modCount == list.modCount
if (s.equals("b")) {
list.add("d"); // modCount++ (теперь modCount != сохранённое)
}
}
// При следующем вызове next() будет: throw ConcurrentModificationException
Что такое fail-fast итератор?
Fail-fast — это механизм, который сразу обнаруживает конкурентную модификацию коллекции и выбрасывает исключение, вместо того чтобы:
- Молча пропустить элементы
- Вернуть неправильные результаты
- Привести к неопределённому поведению
// Внутренняя реализация ArrayList.iterator():
private class Itr implements Iterator<E> {
int expectedModCount = modCount;
public E next() {
checkForComodification();
// ...
}
final void checkForComodification() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
}
Решение 1: Использовать Iterator.remove()
Это безопасный способ удаления элементов:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if (s.equals("b")) {
iterator.remove(); // Безопасное удаление
}
}
System.out.println(list); // [a, c]
Для добавления элементов используйте ListIterator:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
ListIterator<String> iterator = list.listIterator();
while (iterator.hasNext()) {
String s = iterator.next();
if (s.equals("b")) {
iterator.add("d"); // Безопасное добавление
}
}
System.out.println(list); // [a, b, d, c]
Решение 2: Создать копию списка
Итерируйте по копии, модифицируйте оригинал:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : new ArrayList<>(list)) { // Итерируем по копии
if (s.equals("b")) {
list.add("d"); // Модифицируем оригинал
}
}
System.out.println(list); // [a, b, c, d]
Решение 3: Использовать CopyOnWriteArrayList
Для многопоточного кода, когда много читателей:
import java.util.concurrent.CopyOnWriteArrayList;
List<String> list = new CopyOnWriteArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {
if (s.equals("b")) {
list.add("d"); // Безопасно! Не выбросит исключение
}
}
System.out.println(list); // [a, b, c, d]
Как работает CopyOnWriteArrayList:
- При изменении создаёт копию массива
- Итератор работает с "снимком" данных
- Хороша для частого чтения, редкой записи
- Медленнее для частых изменений
Решение 4: Использовать Stream API (функциональный подход)
Самый современный способ:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
List<String> result = list.stream()
.flatMap(s -> s.equals("b") ? Stream.of(s, "d") : Stream.of(s))
.collect(Collectors.toList());
System.out.println(result); // [a, b, d, c]
Полный пример со всеми методами
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.*;
public class ConcurrentModificationDemo {
// НЕПРАВИЛЬНО - выбросит исключение
static void wrongWay() {
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
try {
for (String s : list) {
if (s.equals("b")) {
list.add("d");
}
}
} catch (ConcurrentModificationException e) {
System.out.println("Ошибка: " + e.getMessage());
}
}
// Способ 1: Iterator.remove()
static void methodIteratorRemove() {
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if (s.equals("a")) {
iterator.remove();
}
}
System.out.println("Iterator.remove(): " + list);
}
// Способ 2: Копия списка
static void methodCopy() {
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : new ArrayList<>(list)) {
if (s.equals("b")) {
list.add("d");
}
}
System.out.println("Копия списка: " + list);
}
// Способ 3: CopyOnWriteArrayList
static void methodCopyOnWrite() {
List<String> list = new CopyOnWriteArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {
if (s.equals("b")) {
list.add("d");
}
}
System.out.println("CopyOnWriteArrayList: " + list);
}
// Способ 4: Stream API
static void methodStream() {
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
List<String> result = list.stream()
.flatMap(s -> s.equals("b") ? Stream.of(s, "d") : Stream.of(s))
.collect(Collectors.toList());
System.out.println("Stream API: " + result);
}
public static void main(String[] args) {
wrongWay();
methodIteratorRemove();
methodCopy();
methodCopyOnWrite();
methodStream();
}
}
Сравнение подходов
| Метод | Безопасность | Производительность | Использование |
|---|---|---|---|
| Iterator.remove() | Да | Хорошая | Удаление элементов |
| Копия списка | Да | Медленнее | Простой случай |
| CopyOnWriteArrayList | Да | Медленно для записи | Много читателей |
| Stream API | Да | Хорошая | Функциональный стиль |
Ключевые выводы
- Никогда не модифицируйте список напрямую во время итерации — используйте
Iterator.remove() - Fail-fast механизм защищает от неопределённого поведения
- Для многопоточного кода используйте
CopyOnWriteArrayList - Stream API часто элегантнее для трансформаций
- ListIterator позволяет добавлять элементы безопасно