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

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

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,

35 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

List<Object> l = ...

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

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

}

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

Теперь вы можете удалить с помощью следующего кода

l.removeIf(current -> current == 5);

Исключение одновременной модификации

  1. Один поток
      Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String value = iter.next()
    if (value == "A") {
        //throws ConcurrentModificationException
        list.remove(it.next());
    }
}

Решение: итератор remove() метод

      Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String value = iter.next()
    if (value == "A") {
        it.remove()
    }
}
  1. Многопотоковый
  • копировать / конвертировать и перебирать еще одну коллекцию. Для небольших коллекций
  • synchronize [О]
  • потокобезопасная коллекция [О программе]

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

Вы можете использовать цикл while.

      Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while(iterator.hasNext()){
    Map.Entry<String, String> entry = iterator.next();
    if(entry.getKey().equals("test")) {
        iterator.remove();
    } 
}

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

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

//"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;
}

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

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

Я закончил с этим ConcurrentModificationException, при итерации списка с помощью stream().map()метод. Тем не менее for(:) не генерировал исключение при повторении и изменении списка.

Вот фрагмент кода, если он кому-то поможет: здесь я повторяю ArrayList<BuildEntity> и изменив его с помощью list.remove(obj)

       for(BuildEntity build : uniqueBuildEntities){
            if(build!=null){
                if(isBuildCrashedWithErrors(build)){
                    log.info("The following build crashed with errors ,  will not be persisted -> \n{}"
                            ,build.getBuildUrl());
                    uniqueBuildEntities.remove(build);
                    if (uniqueBuildEntities.isEmpty()) return  EMPTY_LIST;
                }
            }
        }
        if(uniqueBuildEntities.size()>0) {
            dbEntries.addAll(uniqueBuildEntities);
        }
for (Integer i : l)
{
    if (i.intValue() == 5){
            itemsToRemove.add(i);
            break;
    }
}

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

Ура!

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

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

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

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

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

      for (i in myList.size-1 downTo 0) {
    myList.getOrNull(i)?.also {
       if (it == 5)
          myList.remove(it)
    }
}

При использовании HashMap в более новых версиях Java (8+) вы можете выбрать каждый из 3 вариантов:

      public class UserProfileEntity {
    private String Code;
    private String mobileNumber;
    private LocalDateTime inputDT;
    // getters and setters here
}
HashMap<String, UserProfileEntity> upMap = new HashMap<>();


// remove by value
upMap.values().removeIf(value -> !value.getCode().contains("0005"));

// remove by key
upMap.keySet().removeIf(key -> key.contentEquals("testUser"));

// remove by entry / key + value
upMap.entrySet().removeIf(entry -> (entry.getKey().endsWith("admin") || entry.getValue().getInputDT().isBefore(LocalDateTime.now().minusMinutes(3)));

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

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

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

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

FWIW мы также видим 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;
    }
}

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

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);
}
Другие вопросы по тегам