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

1123

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

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

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

public static void main(String[] args) {
    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));
    }

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

    System.out.println(l);
}

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

Exception in thread "main" java.util.ConcurrentModificationException

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

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

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

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

25 ответов

1576
Лучший ответ

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 при переборе ее содержимого.

  • 12
    Что если вы хотите удалить элемент, отличный от элемента, возвращенного в текущей итерации?
  • 0
    Вы должны использовать .remove в итераторе, и он может удалить только текущий элемент, так что нет :)
Показать ещё 10 комментариев
327

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

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

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

  • 38
    цикл foreach является синтаксическим сахаром для повторения. Однако, как вы указали, вам нужно вызывать remove на итераторе - к которому foreach не дает доступа. Отсюда причина, по которой вы не можете удалить в цикле foreach (даже если вы на самом деле используете итератор под капотом)
  • 35
    +1, например, код для использования iter.remove () в контексте, который ответ Билла К [непосредственно] не имеет.
161

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

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

coll.removeIf(i -> i.intValue() == 5);
  • 2
    Ооооо! Я надеялся, что что-то в Java 8 или 9 может помочь. Это все еще кажется довольно многословным для меня, но мне все еще нравится это.
  • 0
    Реализация equals () также рекомендуется в этом случае?
Показать ещё 3 комментария
38

Поскольку вопрос уже был отвечен, лучший способ - использовать метод удаления объекта итератора, я хотел бы перейти к особенностям места, где выдается ошибка "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.

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

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

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);
}
  • 7
    это то, что я обычно делаю, но явный итератор - более элегантное решение, которое я чувствую.
  • 1
    Справедливо, если вы ничего не делаете с итератором - если он открыт, это упрощает такие вещи, как вызов .next () дважды за цикл и т. Д. Не большая проблема, но может вызвать проблемы, если вы делаете что-нибудь более сложное, чем просто просмотр списка для удаления записей.
Показать ещё 4 комментария
17

В таких случаях общий трюк (был?) для возврата назад:

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

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

  • 2
    Это хороший трюк. Но он не будет работать с неиндексированными коллекциями, такими как наборы, и будет очень медленным, скажем, со связанными списками.
  • 0
    @Claudiu Да, это определенно только для ArrayList или подобных коллекций.
Показать ещё 2 комментария
16

Тот же ответ, что и Claudius с циклом for:

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

С Коллекции Eclipse (ранее Коллекции GS), метод 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.

7

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

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++;
      }
  • 0
    Ах, на самом деле это просто расширенный -for-loop, который создает исключение.
  • 0
    FWIW - тот же код будет работать после изменения, чтобы увеличить i++ в защите цикла, а не в теле цикла.
Показать ещё 1 комментарий
7

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

for (String str : new ArrayList<String>(listOfStr))     
{
    listOfStr.remove(/* object reference or index */);
}
  • 17
    Создание копии звучит как пустая трата ресурсов.
  • 3
    @Antzi Это зависит от размера списка и плотности объектов внутри. Все еще ценное и правильное решение.
Показать ещё 1 комментарий
6

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

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

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);
   }
}
  • 0
    Интересны дополнительные методы в интерфейсе ListIterator (расширение Iterator) - особенно его previous метод.
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);
    }
1

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

  • 0
    Да, и обратите внимание, что все они находятся в пакете java.util.concurrent . Некоторые другие похожие / общие CopyOnWriteArrayList использования из этого пакета - CopyOnWriteArrayList & CopyOnWriteArraySet [но не ограничиваются ими].
  • 0
    На самом деле, я только что узнал, что хотя эти объекты структуры данных избегают ConcurrentModificationException , их использование в расширенном цикле -for может по-прежнему вызывать проблемы с индексированием (т. Е. По-прежнему пропускать элементы или IndexOutOfBoundsException ...)
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;
}

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

  • 1
    В этом вопросе прямо говорится, что OP не нужен при использовании ArrayList и, следовательно, не может полагаться на get() . В противном случае, вероятно, хороший подход, хотя.
  • 0
    (Уточнение ^) OP использует произвольную Collection - интерфейс Collection не включает get . (Хотя интерфейс List FWIW включает «get»).
Показать ещё 1 комментарий
0

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

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

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 не может).

FWIW мы также видим, get называют по list, который не может быть сделано, если его ссылка была просто Collection (вместо более определенного List -type из 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;
    }
}
  • 0
    Это все еще требует очень осторожного вычисления признаков, чтобы удалить, как бы то ни было.
  • 0
    Также это просто более подробное объяснение этого ответа stackoverflow.com/a/43441822/2308683
Показать ещё 1 комментарий
0

Я знаю, что этот вопрос слишком старый, чтобы быть о 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);
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
List<String> strings=new ArrayList<String>(){};

while(strings.size() > 0) {

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

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

Ура!

0

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

Время 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 msec --- лучший
  • для итератора: 1043 мс
-1
Collection<Integer> l = new ArrayList<Integer>();//Do the collection thing...

l.removeIf(i -> i == 5);      //iterates through the collection and removes every occurence of 5

Лямбда-выражения и методы Collection в Jdk 8 включены в Handy и добавляют синтаксический сахар sugar.

Метод removeIf просматривает коллекцию и фильтрует ее с помощью Predicate. Предикат является функцией аргумента, который возвращает логическое значение... Так же, как boolean _bool = (str) → str.equals("text");

  • 0
    Хотя этот фрагмент кода может быть решением, включение пояснения действительно помогает улучшить качество вашего сообщения. Помните, что вы отвечаете на вопрос читателей в будущем, и эти люди могут не знать причин, по которым вы предлагаете код.
  • 2
    Это дублирует тот же самый Ответ, который был здесь 4 года назад @ assylias (похоже, что вы даже скопировали логику предикатов). Пожалуйста, не добавляйте дублирующий беспорядок, как этот (получите репутацию другими способами, которые являются законными - о, и посмотрите на это: вы потеряли репутацию из этого Ответа, получив отрицательный результат в голосах ...).
-1

В дополнение к @assylias answer вы также можете использовать новый Stream api, если вы используете Java 8:

List<Integer> l = Arrays.asList(4, 5, 6);

static boolean condition(Integer i) {
    return i == 5;
}

static Predicate<Integer> predicate = YourClassName::condition;

l.stream()
    .filter(predicate.negate())
    .forEach(System.out::println);

Если вы инвертируете условие, решение становится еще более кратким, поскольку вам не нужно negate() предикат, что позволяет использовать только ссылку на метод:

List<Integer> l = Arrays.asList(4, 5, 6);

static boolean condition(Integer i) {
    return i != 5;    // <-- condition has been negated
}

l.stream()
    .filter(YourClassName::condition)
    .forEach(System.out::println);

Одна из красот этого заключается в том, что поток оценивается лениво, т.е. операция filter() фактически не оценивается до тех пор, пока она не будет использована терминальной операцией, такой как forEach(). Подробнее об этом можно узнать в Oracle Tutorial.

-3

Вы можете перебирать список, используя for-loop, и вам нужно вызвать list.remove(0). Вам нужно с жестким кодом индексировать индексный параметр remove с нулем. См. Также этот ответ:

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

list.add(1);
list.add(2);
list.add(3);
list.add(4);
int list_size = list.size();
for (int i = 0; i < list_size; i++) {
    list.remove(0);
}
-3

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню