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


1194

Мы все знаем, что вы не можете сделать следующее из-за ConcurrentModificationException:

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

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

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.


Примечание для читателей: прочитайте docs.oracle.com/javase/tutorial/collections/interfaces/… , это может быть более простой способ достичь того, что вы хотите сделать.
GKFX

Ответы:


1601

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 - она ​​предназначена для этого.


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


19
Что если вы хотите удалить элемент, отличный от элемента, возвращенного в текущей итерации?
Ойген,

2
Вы должны использовать .remove в итераторе, и он может удалить только текущий элемент, поэтому нет :)
Bill K

1
Имейте в виду, что это медленнее по сравнению с использованием ConcurrentLinkedDeque или CopyOnWriteArrayList (по крайней мере, в моем случае)
Дан

1
Разве невозможно поместить iterator.next()вызов в цикл for? Если нет, может кто-нибудь объяснить, почему?
Блейк

1
@GonenI Это реализовано для всех итераторов из коллекций, которые не являются неизменяемыми. List.addв этом же смысле «необязателен», но вы бы не сказали, что «небезопасно» добавлять в список.
Radiodef

345

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

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

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


43
цикл foreach является синтаксическим сахаром для повторения. Однако, как вы указали, вы должны вызывать remove на итераторе - к которому foreach не дает доступа. Следовательно, причина , почему вы не можете удалить в цикле Еогеасп (даже если вы являются на самом деле с помощью итератора под капотом)
madlep

36
+1, например, код для использования iter.remove () в контексте, который ответ Билла К [непосредственно] не имеет.
Издано

202

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

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

coll.removeIf(i -> i == 5);

3
Ооооо! Я надеялся, что что-то в Java 8 или 9 может помочь. Это все еще кажется довольно многословным для меня, но мне все еще нравится это.
Джеймс Т Снелл

Реализация equals () также рекомендуется в этом случае?
Анмол Гупта

кстати removeIfиспользует Iteratorи whileцикл. Вы можете увидеть это на java.util.Collection.java
Ява

3
@omerhakanbilici Некоторые реализации, такие как ArrayListпереопределение по соображениям производительности. То, на что вы ссылаетесь, является только реализацией по умолчанию.
Дидье Л

@AnmolGupta: Нет, здесь equalsвообще не используется, поэтому его не нужно реализовывать. (Но, конечно, если вы используете equalsв своем тесте, то он должен быть реализован так, как вы этого хотите.)
Lii

42

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

Каждый класс коллекции есть отдельный класс , который реализует интерфейс итератора и предоставляет методы , как 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.


Очень интересно. Спасибо! Я часто сам не вызываю remove (), я предпочитаю очищать коллекцию после ее повторения. Не сказать, что это хорошая модель, просто то, что я делал в последнее время.
Джеймс Т Снелл

26

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

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

    l.removeAll(itemsToRemove);
    System.out.println(l);
}

7
это то, что я обычно делаю, но явный итератор - более элегантное решение, которое я чувствую.
Клавдиу

1
Справедливо, если вы ничего не делаете с итератором - если он открыт, это упрощает такие вещи, как вызов .next () дважды за цикл и т. Д. Не большая проблема, но может вызвать проблемы, если вы делаете что-нибудь более сложное, чем просто просмотр списка для удаления записей.
RodeoClown

@RodeoClown: в исходном вопросе Клаудиу удаляет из Коллекции, а не итератор.
Мэтт б

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

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

17

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

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

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


2
Это хороший трюк. Но он не будет работать с неиндексированными коллекциями, такими как наборы, и будет очень медленным, скажем, со связанными списками.
Клавдиу

@Claudiu Да, это определенно только для ArrayLists или подобных коллекций.
Landei

Я использую ArrayList, это сработало отлично, спасибо.
StarSweeper

2
показатели отличные. Если это так часто, почему бы вам не использовать for(int i = l.size(); i-->0;) {?
Джон

16

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

for (Iterator<Object> it = objects.iterator(); it.hasNext();) {
    Object object = it.next();
    if (test) {
        it.remove();
    }
}

12

В Eclipse 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()потому что removeIfв java.util.Collectionинтерфейсе Java 8 был добавлен метод по умолчанию .

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


10

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

for (String str : new ArrayList<String>(listOfStr))     
{
    listOfStr.remove(/* object reference or index */);
}

19
Создание копии звучит как пустая трата ресурсов.
Antzi

3
@Antzi Это зависит от размера списка и плотности объектов внутри. Все еще ценное и правильное решение.
MRE

Я использовал этот метод. Это займет немного больше ресурсов, но гораздо более гибко и понятно.
Тао Чжан,

Это хорошее решение, когда вы не собираетесь удалять объекты внутри самого цикла, но они скорее «случайным образом» удаляются из других потоков (например, сетевые операции, обновляющие данные). Если вы обнаружите, что делаете много копий, то даже Java-реализация делает именно это: docs.oracle.com/javase/8/docs/api/java/util/concurrent/…
A1m

8

Люди утверждают, что нельзя удалить из коллекции, повторяемой циклом 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в коде выше.

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


8

С традиционным для цикла

ArrayList<String> myArray = new ArrayList<>();

for (int i = 0; i < myArray.size(); ) {
    String text = myArray.get(i);
    if (someCondition(text))
        myArray.remove(i);
    else
        i++;   
}

Ах, так это действительно просто расширенный -for-loop, который выбрасывает исключение.
cellepo

FWIW - тот же код все еще будет работать после изменения для увеличения i++защиты цикла, а не внутри тела цикла.
cellepo

Исправление ^: То есть если i++приращение не было условным - теперь я вижу, поэтому вы делаете это в теле :)
cellepo

2

A ListIteratorпозволяет добавлять или удалять элементы в списке. Предположим, у вас есть список Carобъектов:

List<Car> cars = ArrayList<>();
// add cars here...

for (ListIterator<Car> carIterator = cars.listIterator();  carIterator.hasNext(); )
{
   if (<some-condition>)
   { 
      carIterator().remove()
   }
   else if (<some-other-condition>)
   { 
      carIterator().add(aNewCar);
   }
}

Интересны дополнительные методы в интерфейсе ListIterator (расширение Iterator), особенно его previousметод.
cellepo

1

У меня есть предложение по проблеме выше. Нет необходимости вторичного списка или дополнительного времени. Пожалуйста, найдите пример, который сделал бы то же самое, но по-другому.

//"list" is ArrayList<Object>
//"state" is some boolean variable, which when set to true, Object will be removed from the list
int index = 0;
while(index < list.size()) {
    Object r = list.get(index);
    if( state ) {
        list.remove(index);
        index = 0;
        continue;
    }
    index += 1;
}

Это позволит избежать исключения параллелизма.


1
В этом вопросе прямо говорится, что использование ОП необязательно, ArrayListи поэтому на него нельзя положиться get(). В противном случае, вероятно, хороший подход, хотя.
Каскелотти

(Уточнение ^) OP использует произвольный Collection- Collectionинтерфейс не включает get. (Хотя Listинтерфейс FWIW включает «get»).
виолончель

Я просто добавил отдельный, более подробный ответ здесь также для while-looping a List. Но +1 за этот ответ, потому что он пришел первым.
целлепо

1

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


Да, и обратите внимание, что это все в java.util.concurrentупаковке. Некоторые другие похожие / общие сценарии использования из этого пакета являются CopyOnWriteArrayList & CopyOnWriteArraySet [но не ограничиваются ими].
cellepo

На самом деле, я только что узнал, что хотя эти объекты данных избегают этих структур данных ConcurrentModificationException, их использование в расширенном цикле IndexOutOfBoundsException
-for

1

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

Collection<Integer> l = 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));
}

l.removeIf(i -> i.intValue() == 5);

1

Другой способ - создать копию вашего arrayList:

List<Object> l = ...

List<Object> iterationList = ImmutableList.copyOf(l);

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

Примечание: iэто не объект, indexа объект. Возможно, назвать это objбыло бы более уместно.
luckydonald

1

Лучший способ (рекомендуется) - использование пакета java.util.Concurrent. Используя этот пакет, вы можете легко избежать этого исключения. см. Модифицированный код

public static void main(String[] args) {
    Collection<Integer> l = new CopyOnWriteArrayList<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) {
            l.remove(i);
        }
    }

    System.out.println(l);
}

0

В случае ArrayList: remove (int index) - if (index - позиция последнего элемента), он избегает без System.arraycopy()и не требует для этого времени.

время массива увеличивается, если (индекс уменьшается), кстати, элементы списка также уменьшаются!

лучший эффективный способ удаления - удаление его элементов в порядке убывания: while(list.size()>0)list.remove(list.size()-1);// принимает O (1) while(list.size()>0)list.remove(0);// принимает O (factorial (n))

//region prepare data
ArrayList<Integer> ints = new ArrayList<Integer>();
ArrayList<Integer> toRemove = new ArrayList<Integer>();
Random rdm = new Random();
long millis;
for (int i = 0; i < 100000; i++) {
    Integer integer = rdm.nextInt();
    ints.add(integer);
}
ArrayList<Integer> intsForIndex = new ArrayList<Integer>(ints);
ArrayList<Integer> intsDescIndex = new ArrayList<Integer>(ints);
ArrayList<Integer> intsIterator = new ArrayList<Integer>(ints);
//endregion

// region for index
millis = System.currentTimeMillis();
for (int i = 0; i < intsForIndex.size(); i++) 
   if (intsForIndex.get(i) % 2 == 0) intsForIndex.remove(i--);
System.out.println(System.currentTimeMillis() - millis);
// endregion

// region for index desc
millis = System.currentTimeMillis();
for (int i = intsDescIndex.size() - 1; i >= 0; i--) 
   if (intsDescIndex.get(i) % 2 == 0) intsDescIndex.remove(i);
System.out.println(System.currentTimeMillis() - millis);
//endregion

// region iterator
millis = System.currentTimeMillis();
for (Iterator<Integer> iterator = intsIterator.iterator(); iterator.hasNext(); )
    if (iterator.next() % 2 == 0) iterator.remove();
System.out.println(System.currentTimeMillis() - millis);
//endregion
  • для индексной петли: 1090 мсек
  • для индекса desc: 519 мсек --- лучший
  • для итератора: 1043 мсек

0
for (Integer i : l)
{
    if (i.intValue() == 5){
            itemsToRemove.add(i);
            break;
    }
}

Улов будет после удаления элемента из списка, если вы пропустите внутренний вызов iterator.next (). это все еще работает! Хотя я не предлагаю писать такой код, это помогает понять концепцию, стоящую за ним :-)

Ура!


0

Пример модификации потока безопасной коллекции:

public class Example {
    private final List<String> queue = Collections.synchronizedList(new ArrayList<String>());

    public void removeFromQueue() {
        synchronized (queue) {
            Iterator<String> iterator = queue.iterator();
            String string = iterator.next();
            if (string.isEmpty()) {
                iterator.remove();
            }
        }
    }
}

0

Я знаю, что этот вопрос предполагает просто Collection, а не конкретнее List. Но для тех, кто читает этот вопрос и которые действительно работают со Listссылкой, вы можете вместо этого ConcurrentModificationExceptionиспользовать while-loop (изменяя его), если вы хотите избежатьIterator (либо если вы хотите избежать этого в целом, либо избегать его специально для достижения порядок зацикливания, отличный от начала и до конца в каждом элементе [который, я считаю, является единственным порядкомIterator который может сделать сам)]:

* Обновление: см. Комментарии ниже, которые поясняют, что аналогичное также возможно с традиционным циклом -for.

final List<Integer> list = new ArrayList<>();
for(int i = 0; i < 10; ++i){
    list.add(i);
}

int i = 1;
while(i < list.size()){
    if(list.get(i) % 2 == 0){
        list.remove(i++);

    } else {
        i += 2;
    }
}

Нет ConcurrentModificationException из этого кода.

Там мы видим, что цикл не начинается в начале и не останавливается на каждом элементе (что, я считаю, Iteratorсамо по себе не может).

Мы также видим, getчто вызывается list, что нельзя было бы сделать, если бы его ссылка была просто Collection(вместо более конкретного Listтипа Collection) - Listинтерфейс включает get, а Collectionинтерфейс - нет. Если бы не это различие, то listссылка могла бы вместо этого быть Collection[и, следовательно, технически этот Ответ был бы тогда прямым ответом, а не тангенциальным ответом].

FWIWW тот же код по-прежнему работает после изменения, чтобы начать с начала и до остановки на каждом элементе (как Iteratorпорядок):

final List<Integer> list = new ArrayList<>();
for(int i = 0; i < 10; ++i){
    list.add(i);
}

int i = 0;
while(i < list.size()){
    if(list.get(i) % 2 == 0){
        list.remove(i);

    } else {
        ++i;
    }
}

Это все еще требует очень осторожного вычисления признаков, чтобы удалить, как бы то ни было.
OneCricketeer

Кроме того, это просто более подробное объяснение этого ответа stackoverflow.com/a/43441822/2308683
OneCricketeer

Полезно знать - спасибо! Тот другой ответ помог мне понять , что это расширение -дль-петли , что бы бросить ConcurrentModificationException, но не традиционный -дль петля (которые другие ответ использование) - не понимая , что перед тем , как , почему я был мотивирован , чтобы написать этот ответ (я ошибочно подумал тогда, что это все циклы for, которые будут выбрасывать исключение).
cellepo

0

Одним из решений может быть поворот списка и удаление первого элемента, чтобы избежать исключения ConcurrentModificationException или IndexOutOfBoundsException

int n = list.size();
for(int j=0;j<n;j++){
    //you can also put a condition before remove
    list.remove(0);
    Collections.rotate(list, 1);
}
Collections.rotate(list, -1);

0

Попробуйте это (удаляет все элементы в списке, которые равны i):

for (Object i : l) {
    if (condition(i)) {
        l = (l.stream().filter((a) -> a != i)).collect(Collectors.toList());
    }
}

0

Вы также можете использовать рекурсию

Рекурсия в Java - это процесс, в котором метод вызывает себя непрерывно. Метод в Java, который вызывает себя, называется рекурсивным методом.


-2

это может быть не лучшим способом, но для большинства небольших случаев это должно быть приемлемо:

"создать второй пустой массив и добавить только те, которые вы хотите сохранить"

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.