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

Мы все знаем, что вы не можете сделать следующее из-за 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.

Ответ 1

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

Ответ 2

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

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

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

Ответ 3

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

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

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

Ответ 4

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

Ответ 5

Вы можете либо использовать итератор прямо так, как вы упомянули, либо сохранить вторую коллекцию и добавить каждый элемент, который хотите удалить, в новую коллекцию, а затем удалить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);
}

Ответ 6

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

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

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

Ответ 7

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

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

Ответ 8

С Коллекции 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.

Ответ 9

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

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

Ответ 10

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

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++;
      }

Ответ 11

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

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

Ответ 12

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);
   }
}

Ответ 13

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

//"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.

Ответ 14

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

Ответ 15

Лучший способ (рекомендуется) - использование пакета 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);
    }

Ответ 16

Я знаю, что этот вопрос слишком старый, чтобы быть о 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);

Ответ 17

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

List<Object> l = ...

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

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

}

Ответ 18

В случае 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 мс

Ответ 19

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

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

Ура!

Ответ 20

List<String> strings=new ArrayList<String>(){};

while(strings.size() > 0) {

 String str = strings.remove(0);
}

Ответ 21

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

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();
            }
        }
    }
}

Ответ 22

Я знаю, что этот вопрос предполагает только 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;
    }
}

Ответ 23

Одним из решений может быть поворот списка и удаление первого элемента, чтобы избежать исключения 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);

Ответ 24

В дополнение к @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.

Ответ 25

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

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

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

Ответ 26

Вы можете перебирать список, используя 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);
}

Ответ 27

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");