Получение исключения ConcurrentModificationException при удалении элемента из java.util.List во время итерации списка?

@Test
public void testListCur(){
    List<String> li=new ArrayList<String>();
    for(int i=0;i<10;i++){
        li.add("str"+i);
    }

    for(String st:li){
        if(st.equalsIgnoreCase("str3"))
            li.remove("str3");
    }
    System.out.println(li);
}

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

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

Мне интересно, это общая проблема с коллекциями и удалением элементов?

11 ответов

Решение

Я считаю, что это цель метода Iterator.remove(), чтобы иметь возможность удалять элемент из коллекции во время итерации.

Например:

Iterator<String> iter = li.iterator();
while(iter.hasNext()){
    if(iter.next().equalsIgnoreCase("str3"))
        iter.remove();
}

Java 8 способ удалить его из списка без итератора:

li.removeIf(<predicate>)

т.е.

List<String> li = new ArrayList<String>();
// ...
li = li.removeIf(st -> !st.equalsIgnoreCase("str3"));

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

Взято из http://download.oracle.com/javase/1.4.2/docs/api/java/util/ConcurrentModificationException.html

Да, люди сталкиваются с этим - проблема в том, что вы не можете изменить список, перебирая его. Я использовал 2 альтернативы в прошлом:

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

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

В вашем конкретном случае вам даже не нужно повторять, так как вы можете просто использовать removeAll. Посмотрите на API здесь. Есть также изящные методы, такие как retainAll, которые отбрасывают все, что не входит в аргумент. Вы можете использовать методы удаления / сохранения, как только объекты в списке реализуют равно и хэш-код правильно. Если вы не можете полагаться на метод equals / hashcode для определения равенства между экземплярами в вашем приложении, вам придется выполнить удаление самостоятельно....

Попробуйте это (Java 8):

list.removeIf(condition);

Вы можете сделать копию списка, из которого вы хотите удалить элемент, непосредственно в цикле for-each. Для меня это самый простой способ. Что-то вроде этого:

for (String stringIter : new ArrayList<String>(myList)) {
    myList.remove(itemToRemove);
}

Надеюсь, что это поможет вам..

Я думаю, что стоит упомянуть версию Java 8

@Test
public void testListCur() {
    List<String> li = new ArrayList<String>();
    for (int i = 0; i < 10; i++) {
        li.add("str" + i);
    }

    li = li.stream().filter(st -> !st.equalsIgnoreCase("str3")).collect(Collectors.toList());

    System.out.println(li);
}

ArrayList имеет поле modCount - количество модификаций коллекции

Когда вы вызываете метод iterator() создает новый объект Itr, Имеет поле expectedModCount, expectedModCount поле инициализировать с помощью modCount значение. Когда вы вызываете

li.remove("str3");

modCount приращения. Когда вы пытаетесь получить доступ к li через итератор проверяет, что expectedModCount == modCount

и если это ложные броски ConcurrentModificationException

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

У меня возникла эта проблема, и я думаю, что более простой способ такой же, как и второй, который дал hvgotcodes.

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

@Test
public void testListCur(){
    List<String> li=new ArrayList<String>();
    for(int i=0;i<10;i++){
        li.add("str"+i);
    }
    List<String> finalLi = new ArrayList<String>();
    for(String st:li){
        if(st.equalsIgnoreCase("str3")){
            // Do nothing
        } else {
            finalLi.add(st);
        }
    }
    System.out.println(finalLi);
}

Я думаю, что лучший ответ от bigdev.de, но я хотел бы добавить что-то к нему (например, если элемент удален из списка, может быть, вы хотите записать это где-то или что-то):

List<String> list = new ArrayList<>();

list.removeIf(a -> {
                boolean condition = a.equalsIgnoreCase("some condition");
                if(condition)
                    logger.info("Item removed from the list: " + a);
                return condition;
  });

Я перебрал другой путь...

public void testListCur(){
    List<String> li=new ArrayList<String>();
    for(int i=0;i<10;i++){
        li.add("str"+i);
    }

    for(int i=0; i<li.size(); i++)
        if(li.get(i).equalsIgnoreCase("str3"))
            li.remove(i--);

    System.out.println(li);
}
Другие вопросы по тегам