Удаление элемента списка в цикле foreach не бросает ConcurrentModificationException, почему?

273
29 апреля 2018, 22:57

Допустим, у меня есть некий ArrayList.

ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++)
  list.add((int) (Math.random() * 20));

И я хочу из него удалить все числа больше 10.

"Правильно" сделать это можно через итератор, получив гарантированный результат.

for (Iterator<Integer> iterator = list.iterator(); iterator.hasNext(); )
    if (iterator.next() > 10)
        iterator.remove();

Но на мое удивление, корректно работает и вариант:

for (Integer i : list)
    if (i > 10)
        list.remove(i);

Несмотря на то, что везде говорится, что любая попытка удаления из коллекции в цикле без использования итератора приводит к ConcurrentModificationException.

И, действительно, если будем удалять безусловно, получим как раз ConcurrentModificationException

for (Integer i : list)
      list.remove(i);

Собственно, может кто-нибудь объяснить магию или дать наводку, где можно почитать на эту тему?

Answer 1

Видимо вам сказочно повезло:

  • у вас не было элементов, значение которых было больше 10
  • у вас был элемент, значение которого было больше 10, но он был только один и располагался на предпоследнем месте в списке

Давайте рассмотрим работу на более коротком примере.

ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(22);
list.add(3);

Мы добавили три числа. Итак, как работает foreach

  1. Он получает итератор.
  2. Проверяет на наличие следующего элемента hasNext().

    public boolean hasNext() {
        return cursor != size(); // cursor is zero initially.
    }
    
  3. Если возвращается true, то берет следующий элемент с помощьюnext().

    public E next() {
        checkForComodification();
        try {
            E next = get(cursor);
            lastRet = cursor++;
            return next;
        } catch (IndexOutOfBoundsException e) {
            checkForComodification();
            throw new NoSuchElementException();
        }
    }
    final void checkForComodification() {
        // Initially modCount = expectedModCount (our case 5)
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
    

Далее повторяются шаги 2 и 3 пока hasNext() не вернет false.

Если удалить элемент из списка, то его размер уменьшится и modCount увеличится.

Если удалить элемент во время итерации, то будет выброшено ConcurrentModificationException исключение на строке modCount != expectedModCount.

Но что происходит, если удаляется предпоследний элемент?

> cursor = 0 size = 3 --> hasNext() успешно и next() тоже без эксепшена
> cursor = 1 size = 3 --> hasNext() успешно и next() тоже без эксепшена

Когда мы удалим значение 22, то размер уменьшится до 2.

> cursor = 2 size = 2 --> hasNext() не успешно и next() пропускается.

В других же случаях будет выброшено ConcurrentModificationException из-за modCount != expectedModCount.

А в этом единичном случае проверка пройдет на ура..

Вот магия....Или баг....

Answer 2

Да нечего читать на эту тему. В документации ясно сказано, что для удаления элeментов из коллекции нужно использовать итератор.

Тот факт, что в вашем примере вы смогли удалить числа больше 10 обусловлен скорее всего тeм, что там не было таких чисел или было только одно - предпоследнее.
Хотя реалиция foreach основана на использовании итератора, но если вы представите что внутри foreach обыкновенный for (int i = 0; i < 10; i++) то я думаю будет очевидно почему нельзя удалять элементы из массива пока вы его обходите в цикле.

А вообще, используйте Java 8, и забудьте о проблемах :)

List<Integer> list = new ArrayList();
list.removeIf(e -> e > 10);
READ ALSO
Сортировка Map по значению из value

Сортировка Map по значению из value

Есть Map<String, Warp>У класса Warp есть метод getVisits();

260
Подгон изображения по размерам jLabel

Подгон изображения по размерам jLabel

Нужно чтобы изображение которое загружалось jLable в приняло размеры такие как сам компонент jLabel Вот код, что здесь нужно добавить?

168
Захват фокуса Recycle View

Захват фокуса Recycle View

Имеется viewpaget, он имеет 3 фрагментаНа центральном фрагменте (2) у нас есть горизонтальный кастомный recycler view с карточками

228
Как можно открыть 2Gis из своего приложения

Как можно открыть 2Gis из своего приложения

Как можно открыть 2Gis из своего приложения и сразу передать адреса(начальная точка и конечная) чтобы построить маршрут?

191