Коллекция бросает или не бросает ConcurrentModificationException на основе содержимого коллекции

Следующий код Java выдает ConcurrentModificationException, как ожидалось:

public class Evil
{
    public static void main(String[] args) {
        Collection<String> c = new ArrayList<String>();
        c.add("lalala");
        c.add("sososo");
        c.add("ahaaha");
        removeLalala(c);
        System.err.println(c);
    }
    private static void removeLalala(Collection<String> c) 
    {
        for (Iterator<String> i = c.iterator(); i.hasNext();) {
            String s = i.next();
            if(s.equals("lalala")) {
                c.remove(s);
            }
        }
    }
}

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

public class Evil {
    public static void main(String[] args) 
    {
        Collection<String> c = new ArrayList<String>();
        c.add("lalala");
        c.add("lalala");
        removeLalala(c);
        System.err.println(c);
    }
    private static void removeLalala(Collection<String> c) {
        for (Iterator<String> i = c.iterator(); i.hasNext();) {
            String s = i.next();
            if(s.equals("lalala")) {
                c.remove(s);
            }
        }
    }
}

Отпечатает вывод "[lalala]". Почему второй пример не бросает ConcurrentModificationException, когда делает первый пример?

Ответы

Ответ 1

Короткий ответ

Поскольку отказоустойчивое поведение итератора не гарантируется.

Длинный ответ

Вы получаете это исключение, потому что вы не можете манипулировать коллекцией, итерации по ней, за исключением итератора.

Плохо:

// we're using iterator
for (Iterator<String> i = c.iterator(); i.hasNext();) {  
    // here, the collection will check it hasn't been modified (in effort to fail fast)
    String s = i.next();
    if(s.equals("lalala")) {
        // s is removed from the collection and the collection will take note it was modified
        c.remove(s);
    }
}

Хорошо:

// we're using iterator
for (Iterator<String> i = c.iterator(); i.hasNext();) {  
    // here, the collection will check it hasn't been modified (in effort to fail fast)
    String s = i.next();
    if(s.equals("lalala")) {
        // s is removed from the collection through iterator, so the iterator knows the collection changed and can resume the iteration
        i.remove();
    }
}

Теперь к "почему": в приведенном выше коде обратите внимание на то, как выполняется проверка модификации - удаление помещает коллекцию как измененную, а следующая итерация проверяет любые изменения и терпит неудачу, если обнаруживает, что коллекция изменена. Еще одна важная вещь: ArrayList (не уверен в других коллекциях) не проверяет на модификацию в hasNext().

Следовательно, могут произойти две странные вещи:

  • Если вы удалите последний элемент во время итерации, ничего не будет выброшено
    • Это потому, что нет "следующего" элемента, поэтому итерация заканчивается до достижения кода проверки модификации
  • Если вы удалите второй-последний элемент, ArrayList.hasNext() также вернет false, потому что итератор current index теперь указывает на последний элемент (бывший второй по-последнему).
    • Так что даже в этом случае после удаления нет элемента "next"

Обратите внимание, что все это соответствует Документация ArrayList:

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

Отредактировано для добавления:

Этот вопрос содержит некоторую информацию о том, почему проверка параллельной модификации не выполняется в hasNext() и выполняется только в next().

Ответ 2

Если вы посмотрите на исходный код тетера ArrayList (частный вложенный класс Itr), вы увидите недостаток в коде.

Код должен быть неудачным, что делается внутри итератора внутри, вызывая checkForComodification(), однако hasNext() не делает этот вызов, вероятно, по соображениям производительности.

Вместо hasNext() просто:

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

Это означает, что когда вы находитесь во втором последнем элементе списка, а затем удаляете элемент (любой элемент), размер уменьшается, а hasNext() думает, что вы находитесь на последнем элементе (который вы weren ' t) и возвращает false, пропуская итерацию последнего элемента без ошибок.

ОЙ!!!!

Ответ 3

Из других ответов вы знаете, что является правильным способом удаления элемента в коллекции во время итерации коллекции. Я даю здесь объяснение основному вопросу. И ответ на ваш вопрос лежит в следующей трассировке стека

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
    at java.util.ArrayList$Itr.next(Unknown Source)
    at com.ii4sm.controller.Evil.removeLalala(Evil.java:23)
    at com.ii4sm.controller.Evil.main(Evil.java:17)

В stacktrace очевидно, что строка i.next(); выдает ошибку. Но когда у вас есть только два элемента в коллекции.

Collection<String> c = new ArrayList<String>();
c.add("lalala");
c.add("lalala");
removeLalala(c);
System.err.println(c);

Когда первый удаляется i.hasNext() возвращает false, а i.next() никогда не выполняется для исключения исключения

Ответ 4

вы должны удалить из iterator (i) не collection (c) напрямую;

попробуйте следующее:

for (Iterator<String> i = c.iterator(); i.hasNext();) {
    String s = i.next();
    if(s.equals("lalala")) {
        i.remove(); //note here, changing c to  i with no parameter.
    }
}

EDIT:

Причина, по которой первая попытка вызывает исключение, а вторая - не просто из-за количества элементов в вашей коллекции.

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

Ответ 5

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

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

Если вы используете код ниже, вы не получите никаких исключений:

private static void removeLalala(Collection<String> c) 
  {
    /*for (Iterator<String> i = c.iterator(); i.hasNext();) {
      String s = i.next();
      if(s.equals("lalala")) {
        c.remove(s);
      }
    }*/

    Iterator<String> it = c.iterator();
    while (it.hasNext()) {
        String st = it.next();
        if (st.equals("lalala")) {
            it.remove();
        }
    }
  }