Итерация по коллекции, избегая исключения ConcurrentModificationException при удалении объектов в цикле - PullRequest
1120 голосов
/ 22 октября 2008

Мы все знаем, что вы не можете сделать это:

for (Object i : l) {
    if (condition(i)) {
        l.remove(i);
    }
}

ConcurrentModificationException и т. Д. ... это иногда работает, но не всегда. Вот некоторый конкретный код:

public static void main(String[] args) {
    Collection<Integer> l = new ArrayList<>();

    for (int i = 0; i < 10; ++i) {
        l.add(4);
        l.add(5);
        l.add(6);
    }

    for (int i : l) {
        if (i == 5) {
            l.remove(i);
        }
    }

    System.out.println(l);
}

Это, конечно, приводит к:

Exception in thread "main" java.util.ConcurrentModificationException

... хотя несколько потоков этого не делают ... Во всяком случае.

Как лучше всего решить эту проблему? Как я могу удалить элемент из коллекции в цикле, не выбрасывая это исключение?

Я также использую здесь произвольный Collection, не обязательно ArrayList, поэтому вы не можете полагаться на get.

Ответы [ 23 ]

1542 голосов
/ 22 октября 2008

Iterator.remove() безопасно, вы можете использовать его следующим образом:

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

// This is a clever way to create the iterator and call iterator.hasNext() like
// you would do in a while-loop. It would be the same as doing:
//     Iterator<String> iterator = list.iterator();
//     while (iterator.hasNext()) {
for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
    String string = iterator.next();
    if (string.isEmpty()) {
        // Remove the current element from the iterator and the list.
        iterator.remove();
    }
}

Обратите внимание, что Iterator.remove() - это единственный безопасный способ изменить коллекцию во время итерации; поведение не определено, если базовая коллекция изменена любым другим способом во время выполнения итерации.

Источник: docs.oracle> Интерфейс коллекции


И точно так же, если у вас есть ListIterator и вы хотите добавить элементов, вы можете использовать ListIterator#add, по той же причине, по которой вы можете использовать Iterator#remove & mdash; оно предназначено для этого.


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

331 голосов
/ 22 октября 2008

Это работает:

Iterator<Integer> iter = l.iterator();
while (iter.hasNext()) {
    if (iter.next() == 5) {
        iter.remove();
    }
}

Я предположил, что, поскольку цикл foreach является синтаксическим сахаром для итерации, использование итератора не поможет ... но он предоставляет вам .remove() функциональность.

185 голосов
/ 28 мая 2014

С Java 8 вы можете использовать новый removeIf метод . Применительно к вашему примеру:

Collection<Integer> coll = new ArrayList<>();
//populate

coll.removeIf(i -> i == 5);
40 голосов
/ 15 мая 2010

Поскольку на этот вопрос уже дан ответ, т. Е. Лучший способ - использовать метод удаления объекта итератора, я бы подробно остановился на том месте, где выдается ошибка "java.util.ConcurrentModificationException".

Каждый класс коллекции имеет закрытый класс, который реализует интерфейс Iterator и предоставляет такие методы, как next(), remove() и hasNext().

Код для следующего выглядит примерно так ...

public E next() {
    checkForComodification();
    try {
        E next = get(cursor);
        lastRet = cursor++;
        return next;
    } catch(IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
    }
}

Здесь метод checkForComodification реализован как

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

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

25 голосов
/ 22 октября 2008

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

public static void main(String[] args)
{
    Collection<Integer> l = new ArrayList<Integer>();
    Collection<Integer> itemsToRemove = new ArrayList<Integer>();
    for (int i=0; i < 10; ++i) {
    l.add(new Integer(4));
    l.add(new Integer(5));
    l.add(new Integer(6));
    }
    for (Integer i : l)
    {
        if (i.intValue() == 5)
            itemsToRemove.add(i);
    }

    l.removeAll(itemsToRemove);
    System.out.println(l);
}
17 голосов
/ 29 августа 2014

В таких случаях обычная хитрость - это (было?) Идти назад:

for(int i = l.size() - 1; i >= 0; i --) {
  if (l.get(i) == 5) {
    l.remove(i);
  }
}

Тем не менее, я более чем счастлив, что у вас есть лучшие способы в Java 8, например removeIf или filter в потоках.

16 голосов
/ 21 августа 2013

Тот же ответ, что и Клавдий с циклом for:

for (Iterator<Object> it = objects.iterator(); it.hasNext();) {
    Object object = it.next();
    if (test) {
        it.remove();
    }
}
11 голосов
/ 19 декабря 2012

С Eclipse Collections (ранее GS Collections ) метод removeIf, определенный для MutableCollection , будет работать:

MutableList<Integer> list = Lists.mutable.of(1, 2, 3, 4, 5);
list.removeIf(Predicates.lessThan(3));
Assert.assertEquals(Lists.mutable.of(3, 4, 5), list);

С помощью синтаксиса Java 8 Lambda это можно записать следующим образом:

MutableList<Integer> list = Lists.mutable.of(1, 2, 3, 4, 5);
list.removeIf(Predicates.cast(integer -> integer < 3));
Assert.assertEquals(Lists.mutable.of(3, 4, 5), list);

Вызов Predicates.cast() необходим здесь, поскольку в интерфейсе java.util.Collection в Java 8 был добавлен метод removeIf по умолчанию.

Примечание: Я являюсь коммиттером для Коллекции Eclipse .

9 голосов
/ 26 июня 2012

Сделайте копию существующего списка и переберите новую копию.

for (String str : new ArrayList<String>(listOfStr))     
{
    listOfStr.remove(/* object reference or index */);
}
7 голосов
/ 17 марта 2018

Люди утверждают, что один не может удалить из коллекции, повторяемой циклом foreach. Я просто хотел указать на то, что технически неверно, и точно описать (я знаю, что вопрос ОП настолько сложен, чтобы не знать этого) код, стоящий за этим предположением:

    for (TouchableObj obj : untouchedSet) {  // <--- This is where ConcurrentModificationException strikes
        if (obj.isTouched()) {
            untouchedSet.remove(obj);
            touchedSt.add(obj);
            break;  // this is key to avoiding returning to the foreach
        }
    }

Дело не в том, что вы не можете удалить из повторного Colletion, скорее, в том, что вы не можете продолжить итерацию, как только это сделаете. Следовательно break в коде выше.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...