Что произойдет если будешь менять коллекцию во время итерирования?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Изменение коллекции во время итерирования
Прямой ответ
Будет выброшено ConcurrentModificationException (в большинстве случаев). Это защитный механизм Java, который предотвращает непредсказуемое поведение при изменении коллекции во время итерирования.
Визуализация проблемы
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
// ❌ ConcurrentModificationException!
for (String item : list) {
if (item.equals("B")) {
list.remove(item); // Изменяем коллекцию во время итерирования
}
}
// Exception in thread "main" java.util.ConcurrentModificationException
Почему это происходит?
Механизм обнаружения через modCount:
// Внутренняя реализация ArrayList
public class ArrayList<E> extends AbstractList<E> {
protected transient int modCount = 0; // Счётчик модификаций
@Override
public boolean add(E e) {
modCount++; // Увеличиваем при каждом изменении
// ... остальной код
}
@Override
public boolean remove(Object o) {
modCount++; // Увеличиваем и здесь
// ... остальной код
}
}
// Итератор проверяет это
private class Itr implements Iterator<E> {
private int expectedModCount = modCount; // Запомнили начальное значение
@Override
public E next() {
checkForComodification(); // Проверяем перед каждым вызовом
// ...
}
private void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException(); // БИНГО!
}
}
Примеры проблемных кодов
Пример 1: Удаление во время for-each
// ❌ ConcurrentModificationException
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
for (Integer num : numbers) {
if (num % 2 == 0) {
numbers.remove(num); // Ошибка!
}
}
Пример 2: Добавление во время итерирования
// ❌ ConcurrentModificationException
List<String> items = new ArrayList<>();
items.add("item1");
items.add("item2");
for (String item : items) {
if (item.equals("item1")) {
items.add("item3"); // Добавляем элемент
}
}
Пример 3: С Iterator явно
// ❌ ConcurrentModificationException
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals("B")) {
list.remove(item); // Ошибка! Даже с явным итератором
}
}
Почему это важно (проблема)
Если бы Java это не проверяла, получались бы баги:
Исходный список: [A, B, C, D, E]
Итератор указывает на индекс 1 (B)
1. Удаляем B → [A, C, D, E]
2. Следующая итерация берёт индекс 2 → получаем D (пропустили C!)
Итог: Пропустили элемент, непредсказуемое поведение
Правильные способы удаления/изменения
Способ 1: Использовать Iterator.remove()
// ✅ Правильно!
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals("B")) {
iterator.remove(); // Iterator.remove() безопасен
}
}
Внутренняя реализация:
private class Itr implements Iterator<E> {
private int lastRet = -1;
@Override
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
try {
ArrayList.this.remove(lastRet);
expectedModCount = modCount; // Обновляем expected!
lastRet = -1;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
Способ 2: Создать новый список с отфильтрованными элементами
// ✅ Правильно! (Java 8+)
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
list.removeAll(list.stream()
.filter(s -> s.equals("B"))
.collect(Collectors.toList()));
ОР ещё лучше:
// ✅ Самый чистый вариант (Java 8+)
list.removeIf(s -> s.equals("B"));
Способ 3: Collections.synchronizedList (для многопоточности)
// Для многопоточного доступа
List<String> syncList = Collections.synchronizedList(
new ArrayList<>(Arrays.asList("A", "B", "C"))
);
// Но всё равно нужна синхронизация при итерировании
synchronized (syncList) {
Iterator<String> iterator = syncList.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals("B")) {
iterator.remove(); // Безопасно
}
}
}
Способ 4: CopyOnWriteArrayList (для многопоточности)
// Для частого чтения, редкого изменения
List<String> list = new CopyOnWriteArrayList<>(
Arrays.asList("A", "B", "C")
);
// Итерирование не выбросит исключение
for (String item : list) {
if (item.equals("B")) {
list.remove(item); // Не выбросит ConcurrentModificationException
}
}
// Но это медленнее, потому что копирует весь список при каждом изменении
Полная таблица коллекций
| Коллекция | Бросает ли ConcurrentModificationException? | Решение |
|---|---|---|
| ArrayList | ✓ Да | Iterator.remove() или removeIf() |
| LinkedList | ✓ Да | Iterator.remove() или removeIf() |
| HashSet | ✓ Да | Iterator.remove() или removeIf() |
| TreeSet | ✓ Да | Iterator.remove() или removeIf() |
| HashMap | ✓ Да | Iterator.remove() |
| Collections.synchronizedList() | ✓ Да (нужна синхронизация) | Iterator.remove() под synchronized |
| CopyOnWriteArrayList | ✗ Нет | Безопасна, но медленнее |
| ConcurrentHashMap | ✗ Нет | Безопасна для итерирования |
Реальный пример из production
Задача: Отправить email-ы и удалить отправленные из очереди
// ❌ Плохо
public void sendEmails(List<Email> queue) {
for (Email email : queue) {
try {
emailService.send(email);
queue.remove(email); // ConcurrentModificationException!
} catch (Exception e) {
log.error("Failed to send", e);
}
}
}
// ✅ Хорошо
public void sendEmails(List<Email> queue) {
Iterator<Email> iterator = queue.iterator();
while (iterator.hasNext()) {
Email email = iterator.next();
try {
emailService.send(email);
iterator.remove(); // Безопасное удаление
} catch (Exception e) {
log.error("Failed to send", e);
}
}
}
// ✅ Или современный вариант
public void sendEmails(List<Email> queue) {
queue.removeIf(email -> {
try {
emailService.send(email);
return true; // Удалить из очереди
} catch (Exception e) {
log.error("Failed to send", e);
return false; // Оставить в очереди
}
});
}
Краткий чеклист
✗ НИКОГДА:
for (T item : collection) { collection.remove/add(...) }Iterator<T> it = col.iterator(); while (it.hasNext()) { col.remove(...) }
✓ ИСПОЛЬЗУЙ:
iterator.remove()collection.removeIf(predicate)collection.removeAll(toRemove)- Для многопоточности:
CopyOnWriteArrayListилиConcurrentHashMap
Главное правило: Если изменяешь коллекцию, изменяй её через Iterator, а не напрямую через коллекцию.