Почему этот код не вызывает исключение ConcurrentModificationException?

Я читал об исключении ConcurrentModificationException и о том, как его избежать. Найдена статья. В первом листинге этой статьи был код, похожий на следующий, что, по-видимому, должно было вызвать исключение:

List<String> myList = new ArrayList<String>();
myList.add("January");
myList.add("February");
myList.add("March");

Iterator<String> it = myList.iterator();
while(it.hasNext())
{
    String item = it.next();
    if("February".equals(item))
    {
        myList.remove(item);
    }
}

for (String item : myList)
{
    System.out.println(item);
}

Затем он продолжил объяснять, как решить проблему с различными предложениями.

Когда я попытался воспроизвести его, я не получил исключения! Почему я не получаю исключение?


person Community    schedule 03.02.2013    source источник


Ответы (4)


Согласно документам Java API Iterator.hasNext не выдает ConcurrentModificationException.

После проверки "January" и "February" вы удаляете один элемент из списка. Вызов it.hasNext() не выдает ConcurrentModificationException, а возвращает false. Таким образом, ваш код выходит чисто. Однако последняя строка никогда не проверяется. Если вы добавите "April" в список, вы получите исключение, как и ожидалось.

import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;

public class Main {
        public static void main(String args[]) {

                List<String> myList = new ArrayList<String>();
                myList.add("January");
                myList.add("February");
                myList.add("March");
                myList.add("April");

                Iterator<String> it = myList.iterator();
                while(it.hasNext())
                {
                    String item = it.next();
                    System.out.println("Checking: " + item);
                    if("February".equals(item))
                    {
                        myList.remove(item);
                    }
                }

                for (String item : myList)
                {
                    System.out.println(item);
                }

        }
}

http://ideone.com/VKhHWN

person bikeshedder    schedule 03.02.2013
comment
Вы прекрасно объяснили, что происходит, но не почему. И почему: это ошибка в классе итератора ArrayList. - person T.J. Crowder; 03.02.2013
comment
Не отвечает на вопрос. Но и не ошибка, как говорит TJ. - person zedoo; 03.02.2013
comment
Сначала я был сбит с толку, но после добавления оператора печати внутрь цикла while все стало ясно. Документация по API Iterator.hasNext не заявляйте, что выбрасывается ConcurrentModificationException, так что это действительно работает так, как задумано. Это немного нелогично, и, если честно, я ожидаю, что hasNext выкинет в таких случаях. Эта проверка, вероятно, была опущена из соображений производительности. - person bikeshedder; 03.02.2013
comment
Я просто добавил ссылку на документы API в свой ответ. Он работает так, как задумано. hasNext не выбрасывает, но и не возвращает true в этом случае. Таким образом, это объясняет поведение и отвечает на вопрос. - person bikeshedder; 03.02.2013
comment
Согласно документации Java API, Iterator.hasNext не выдает ConcurrentModificationException. Facepalm +1, удалил мой ответ. Это СЕРЬЕЗНО неправильно, но четко задокументировано. :-) - person T.J. Crowder; 03.02.2013
comment
На самом деле, даже Iterator.next() не заявляет, что бросает CME. Только в JavaDoc для всего класса ArrayList говорится: если список структурно изменен в любой момент после создания итератора любым способом, кроме как с помощью собственных методов удаления или добавления итератора, итератор выдаст исключение ConcurrentModificationException, но конкретный метод не указан. - person Natix; 03.02.2013
comment
Почему if("March".equals(item)) myList.remove(item); все еще выдает исключение? - person Marcos Vasconcelos; 23.07.2019

Из источника ArrayList (JDK 1.7):

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

Каждая операция изменения ArrayList увеличивает поле modCount (сколько раз список был изменен с момента создания).

Когда итератор создается, он сохраняет текущее значение modCount в expectedModCount. Логика такова:

  • если список вообще не изменяется во время итерации, modCount == expectedModCount
  • если список изменен собственным методом итератора remove(), modCount увеличивается, но expectedModCount также увеличивается, поэтому modCount == expectedModCount все еще сохраняется
  • если какой-либо другой метод (или даже какой-либо другой экземпляр итератора) изменяет список, modCount увеличивается, следовательно, modCount != expectedModCount, что приводит к ConcurrentModificationException

Однако, как видно из исходника, в методе hasNext() проверка не выполняется, только в методе next(). Метод hasNext() также сравнивает только текущий индекс с размером списка. Когда вы удаляли предпоследний элемент из списка ("February"), это приводило к тому, что следующий вызов hasNext() просто возвращал false и завершал итерацию до того, как мог быть запущен CME.

Однако, если вы удалите какой-либо элемент, кроме предпоследнего, возникнет исключение.

person Natix    schedule 03.02.2013

Я думаю, что правильным объяснением является этот отрывок из javadocs ConcurrentModificationExcetion:

Обратите внимание, что безотказное поведение не может быть гарантировано, так как, вообще говоря, невозможно дать какие-либо жесткие гарантии при наличии несинхронизированной параллельной модификации. Быстрые отказоустойчивые операции вызывают исключение ConcurrentModificationException в максимально возможной степени. Поэтому было бы неправильно писать программу, корректность которой зависела бы от этого исключения: ConcurrentModificationException следует использовать только для обнаружения ошибок.

Таким образом, если итератор работает быстро, он может сгенерировать исключение, но это не гарантируется. Попробуйте заменить February на January в вашем примере, и возникнет исключение (по крайней мере, в моей среде)

person zedoo    schedule 03.02.2013
comment
Пока этот абзац существует, обратите внимание на оговорки в нем. Между тем, велосипедшеддер доходит до сути дела: hasNext не бросает ConcurrentModificationException! Перед лицом этого простого факта весь мой анализ в моем ответе (и ваш в нашем) — это пока. - person T.J. Crowder; 03.02.2013

Итератор проверяет, что он повторил столько раз, сколько у вас осталось элементов, чтобы увидеть, что он достиг конца, прежде чем он проверит параллельную модификацию. Это означает, что если вы удалите только предпоследний элемент, вы не увидите CME в том же итераторе.

person Peter Lawrey    schedule 03.02.2013