Какое исключение будет выброшено в случае Fail-Fast в коллекциях?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
ConcurrentModificationException и Fail-Fast итераторы
Это один из тех багов, который мне нравится находить в коде junior разработчиков. Fail-Fast поведение — это защитный механизм Java, который помогает избежать тихих ошибок в многопоточной среде.
Какое исключение выбросится
ConcurrentModificationException — это исключение, которое выбросится при нарушении fail-fast гарантии.
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
try {
for (Integer val : list) {
if (val == 3) {
list.remove(Integer.valueOf(3)); // Изменяем коллекцию
}
}
} catch (ConcurrentModificationException e) {
System.out.println("Exception: " + e); // Будет выброшено!
}
Что такое Fail-Fast
Fail-Fast (быстро падай) — это стратегия, когда итератор обнаруживает изменение коллекции и сразу же выбрасывает исключение, вместо того чтобы позволить коду продолжить работу с испорченными данными.
// Как это работает внутри
private class Itr implements Iterator<E> {
int expectedModCount = modCount; // Сохраняем версию
public E next() {
checkForComodification();
// ...
return list.get(lastRet);
}
final void checkForComodification() {
if (modCount != expectedModCount) // Проверяем версию
throw new ConcurrentModificationException();
}
}
Каждая коллекция имеет счётчик изменений (modCount). Когда итератор создаётся, он сохраняет текущее значение. Если коллекция изменилась — счётчик вырастет, и итератор заметит несоответствие.
Примеры ошибок
1. Удаление элемента во время итерации
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
// НЕПРАВИЛЬНО! Выбросится ConcurrentModificationException
for (String str : list) {
if (str.equals("b")) {
list.remove(str); // Modifiction!
}
}
2. Правильный способ 1: используй Iterator.remove()
// ПРАВИЛЬНО! Iterator.remove() не выбрасывает исключение
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String str = it.next();
if (str.equals("b")) {
it.remove(); // Безопасно! Iterator знает об изменении
}
}
Почему это работает? Потому что Iterator.remove() обновляет expectedModCount:
public void remove() {
checkForComodification();
// Удаляем
list.remove(lastRet);
expectedModCount = modCount; // Обновляем версию!
}
3. Правильный способ 2: используй removeIf()
// ПРАВИЛЬНО! removeIf() внутри себя работает безопасно
list.removeIf(str -> str.equals("b"));
4. Правильный способ 3: создай новый список
// ПРАВИЛЬНО! Не трогаем оригинальный список
list = list.stream()
.filter(str -> !str.equals("b"))
.collect(Collectors.toList());
ConcurrentHashMap не выбрасывает ConcurrentModificationException
Важное уточнение: ConcurrentHashMap разработан для многопоточной среды и НЕ выбросит исключение:
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
// В ConcurrentHashMap это НЕ выбросит исключение
for (String key : map.keySet()) {
if (key.equals("b")) {
map.remove(key); // Никакого исключения!
}
}
Почему? Потому что ConcurrentHashMap использует bucketing и synchronization, а не простой счётчик версий.
Многопоточный сценарий
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
list.addAll(Arrays.asList(1, 2, 3, 4, 5));
Thread t1 = new Thread(() -> {
for (Integer val : list) {
System.out.println(val);
Thread.sleep(10);
}
});
Thread t2 = new Thread(() -> {
Thread.sleep(25);
list.remove(Integer.valueOf(3)); // Другой поток изменяет
});
t1.start();
t2.start();
// Возможно ConcurrentModificationException!
Почему это важно
Fail-Fast поведение спасает от тихих ошибок:
// БЕЗ fail-fast (плохо):
for (User user : users) {
if (user.isInactive()) {
users.remove(user); // Молча пропускаем элементы
// Итератор прыгает через элементы
}
}
// users перестаёт быть в корректном состоянии
// С fail-fast (хорошо):
for (User user : users) {
if (user.isInactive()) {
users.remove(user); // ConcurrentModificationException!
}
}
// Немедленно узнаёшь о проблеме!
Практические рекомендации
- Используй Iterator.remove() для удаления во время итерации:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (shouldRemove(it.next())) {
it.remove();
}
}
- Используй removeIf() для фильтрации:
list.removeIf(item -> item.getValue() == 0);
- Используй Stream API для трансформаций:
list = list.stream()
.filter(item -> item.getValue() != 0)
.collect(Collectors.toList());
-
Не изменяй коллекцию с другого потока без синхронизации
-
Если нужна многопоточность, используй ConcurrentHashMap или CopyOnWriteArrayList
ConcurrentModificationException — это не баг, это feature! Он защищает твой код от коварных ошибок.