Як уникнути java.util.ConcurrentModificationException під час ітерації через та видалення елементів із ArrayList


203

У мене є ArrayList, який я хочу повторити. Під час ітерації над ним я повинен одночасно видаляти елементи. Очевидно, це кидає а java.util.ConcurrentModificationException.

Яка найкраща практика для вирішення цієї проблеми? Чи слід спершу клонувати цей список?

Я видаляю елементи не в самій циклі, а в іншій частині коду.

Мій код виглядає приблизно так:

public class Test() {
    private ArrayList<A> abc = new ArrayList<A>();

    public void doStuff() {
        for (A a : abc) 
        a.doSomething();
    }

    public void removeA(A a) {
        abc.remove(a);
    }
}

a.doSomethingможе зателефонувати Test.removeA();


Відповіді:


325

Два варіанти:

  • Створіть список значень, які ви хочете видалити, додавши до цього списку в циклі, після чого зателефонуйте originalList.removeAll(valuesToRemove)в кінці
  • Використовуйте remove()метод на самому ітераторі. Зауважте, що це означає, що ви не можете використовувати розширений цикл.

Як приклад другого варіанта, видалення зі списку будь-яких рядків довжиною більше 5:

List<String> list = new ArrayList<String>();
...
for (Iterator<String> iterator = list.iterator(); iterator.hasNext(); ) {
    String value = iterator.next();
    if (value.length() > 5) {
        iterator.remove();
    }
}

2
Я мав би згадати, що я видаляю елементи в іншій частині коду, а не сам цикл.
RoflcoptrException

@Roflcoptr: Ну важко відповісти, не бачачи, як взаємодіють два біти коду. В основному, ви не можете цього зробити. Не очевидно, чи допоможе спершу клонування у списку, не бачачи, як усе це звисає. Чи можете ви дати більше деталей у своєму запитанні?
Джон Скіт

Я знаю, що клонування списку допомогло б, але я не знаю, чи це гарний підхід. Але я додам ще трохи коду.
RoflcoptrException

2
Це рішення також призводить до java.util.ConcurrentModificationException, див. Stackoverflow.com/a/18448699/2914140 .
CoolMind

1
@CoolMind: Без кількох потоків цей код повинен бути добре.
Джон Скіт

17

З JavaDocs з ArrayList

Ітератори, повернуті ітератором цього класу та методом listIterator цього класу, є невдалими: якщо список структурно модифікований у будь-який час після створення ітератора, будь-яким способом, за винятком власних методів видалення або додавання ітератора, ітератор викине ConcurrentModificationException.


6
і де відповідь на запитання?
Аделін

Як мовиться, за винятком власних методів видалення або додавання ітератора
Varun

14

Ви намагаєтесь видалити значення зі списку в розширеному "for loop", що неможливо, навіть якщо ви застосували будь-який трюк (який ви зробили у своєму коді). Кращим способом є кодування рівня ітератора, як інше, що рекомендується тут.

Цікаво, як люди не запропонували традиційний для циклу підхід.

for( int i = 0; i < lStringList.size(); i++ )
{
    String lValue = lStringList.get( i );
    if(lValue.equals("_Not_Required"))
    {
         lStringList.remove(lValue);
         i--; 
    }  
}

Це також працює.


2
Це неправильно !!! коли ви видаляєте елемент, наступний займає його положення, і в той час як я збільшує, наступний елемент не перевіряється в наступній ітерації. У цьому випадку вам слід піти на (int i = lStringList.size (); i> -1; i--)
Johntor

1
Погодьтеся! Альтернативою є виконання i--; в тому випадку, якщо стан для циклу.
suhas0sn07

Я думаю, що ця відповідь була відредагована для вирішення питань у вищезазначених коментарях, так як зараз це працює добре, принаймні для мене.
Кіра Резарі

11

Вам слід просто просто повторити масив традиційним способом

Кожен раз, коли ви видаляєте елемент зі списку, елементи після цього будуть висуватися вперед. Поки ви не змінюєте інші елементи, крім повторюваного, наступний код повинен працювати.

public class Test(){
    private ArrayList<A> abc = new ArrayList<A>();

    public void doStuff(){
        for(int i = (abc.size() - 1); i >= 0; i--) 
            abc.get(i).doSomething();
    }

    public void removeA(A a){
        abc.remove(a);
    }
}

10

У Java 8 ви можете використовувати інтерфейс колекції та зробити це, зателефонувавши до методу removeIf:

yourList.removeIf((A a) -> a.value == 2);

Більше інформації можна знайти тут


6

Зробіть цикл звичайним способом, java.util.ConcurrentModificationExceptionце помилка, пов’язана з елементами, до яких можна отримати доступ.

Тому спробуйте:

for(int i = 0; i < list.size(); i++){
    lista.get(i).action();
}

Ви уникали цього java.util.ConcurrentModificationException, не видаляючи нічого зі списку. Хитрий. :) Ви справді не можете назвати це "звичайним способом" для повторення списку.
Zsolt Sky

6

Під час ітерації списку, якщо ви хочете видалити елемент, можливо. Давайте нижче дивіться мої приклади,

ArrayList<String>  names = new ArrayList<String>();
        names.add("abc");
        names.add("def");
        names.add("ghi");
        names.add("xyz");

У мене є перелічені назви списку масивів. І я хочу видалити ім'я "def" із наведеного списку,

for(String name : names){
    if(name.equals("def")){
        names.remove("def");
    }
}

Вищевказаний код кидає виняток ConcurrentModificationException, оскільки ви змінюєте список під час ітерації.

Таким чином, щоб видалити ім'я "def" з Arraylist таким чином,

Iterator<String> itr = names.iterator();            
while(itr.hasNext()){
    String name = itr.next();
    if(name.equals("def")){
        itr.remove();
    }
}

Вищевказаний код, за допомогою ітератора ми можемо видалити ім'я "def" з Arraylist і спробувати надрукувати масив, ви побачите нижченаведений вихід.

Вихід: [abc, ghi, xyz]


В іншому випадку ми можемо використовувати спільний список, який є в одночасному пакеті, щоб ви могли виконувати видалення та додавання операцій під час ітерації. Наприклад, див. Фрагмент коду нижче. ArrayList <String> імена = новий ArrayList <String> (); CopyOnWriteArrayList <String> copyNames = новий CopyOnWriteArrayList <String> (імена); для (ім'я рядка: copyNames) {if (name.equals ("def")) {copyNames.remove ("def"); }}
Індра К

CopyOnWriteArrayList буде найдорожчим операцією.
Індра К

5

Один із варіантів - це змінити removeAметод на це -

public void removeA(A a,Iterator<A> iterator) {
     iterator.remove(a);
     }

Але це означає , що ваш doSomething()повинен бути в змозі пройти iteratorдо removeметоду. Не дуже гарна ідея.

Чи можете ви це зробити у двоступеневому підході. У першому циклі, коли ви повторюєте список, замість того, щоб видаляти вибрані елементи, позначте їх як видалені . Для цього ви можете просто скопіювати ці елементи (дрібну копію) в інший List.

Потім, як тільки ваша ітерація виконана, просто зробіть removeAllз першого списку всі елементи у другому списку.


Чудово, я використовував той самий підхід, хоча два рази петлю. це робить прості речі і жодних проблем із цим не виникає :)
Pankaj Nimgade

1
Я не бачу, що в Iterator є метод видалення (a). У файлі Remove () немає аргументів docs.oracle.com/javase/8/docs/api/java/util/Iterator.html що я пропускаю?
c0der

5

Ось приклад, коли я використовую інший список для додавання об'єктів для видалення, а потім використовую stream.foreach для видалення елементів із початкового списку:

private ObservableList<CustomerTableEntry> customersTableViewItems = FXCollections.observableArrayList();
...
private void removeOutdatedRowsElementsFromCustomerView()
{
    ObjectProperty<TimeStamp> currentTimestamp = new SimpleObjectProperty<>(TimeStamp.getCurrentTime());
    long diff;
    long diffSeconds;
    List<Object> objectsToRemove = new ArrayList<>();
    for(CustomerTableEntry item: customersTableViewItems) {
        diff = currentTimestamp.getValue().getTime() - item.timestamp.getValue().getTime();
        diffSeconds = diff / 1000 % 60;
        if(diffSeconds > 10) {
            // Element has been idle for too long, meaning no communication, hence remove it
            System.out.printf("- Idle element [%s] - will be removed\n", item.getUserName());
            objectsToRemove.add(item);
        }
    }
    objectsToRemove.stream().forEach(o -> customersTableViewItems.remove(o));
}

Я думаю, що ви виконуєте додаткову роботу, виконуючи дві петлі, в гіршому випадку петлі будуть усього списку. Було б найпростіше і дешевше зробити це лише в одній петлі.
Луїс Карлос

Я не думаю, що ви можете видалити об'єкт з першого циклу, отже, необхідність у додатковому циклі видалення, а також цикл видалення - це лише об’єкти для видалення - можливо, ви могли б написати приклад лише одним циклом, я хотів би це побачити - дякую @ LuisCarlos
серуп

Як ви кажете за допомогою цього коду, ви не можете видалити жоден елемент всередині for-loop, оскільки він викликає виняток java.util.ConcurrentModificationException. Однак ви можете використовувати базовий для. Тут я пишу приклад, використовуючи частину вашого коду.
Луїс Карлос

1
for (int i = 0; i <customersTableViewItems.size (); i ++) {diff = currentTimestamp.getValue (). getTime () - customersTableViewItems.get (i) .timestamp.getValue (). getTime (); diffSeconds = diff / 1000% 60; if (diffSeconds> 10) {customersTableViewItems.remove (i--); }} Важливо я - тому що ви не хочете пропускати жодного елементу. Також ви можете використовувати метод removeIf (предикат <? Super E> фільтр), наданий класом ArrayList. Сподіваюсь на цю допомогу
Луїс Карлос

1
Виняток трапляється тому, що в for-loop є активна посилання на ітератор списку. Зазвичай для цього немає посилань, і ви маєте більше гнучкості для зміни даних. Сподіваюся, що це допоможе
Луїс Карлос

3

Замість використання для кожного циклу використовуйте нормальне для циклу. Наприклад, наведений нижче код видаляє всі елементи зі списку масивів, не надаючи java.util.ConcurrentModificationException. Ви можете змінити умову в циклі відповідно до Вашого випадку використання.

   for(int i=0;i<abc.size();i++)  {

          e.remove(i);
        }

2

Зроби щось таке, як це:

for (Object object: (ArrayList<String>) list.clone()) {
    list.remove(object);
}

2

Альтернативне рішення Java 8 за допомогою потоку:

        theList = theList.stream()
            .filter(element -> !shouldBeRemoved(element))
            .collect(Collectors.toList());

У Java 7 замість цього можна використовувати Guava:

        theList = FluentIterable.from(theList)
            .filter(new Predicate<String>() {
                @Override
                public boolean apply(String element) {
                    return !shouldBeRemoved(element);
                }
            })
            .toImmutableList();

Зауважте, що приклад Guava приводить до незмінного списку, який може бути або не бути таким, як ви хочете.


1

Ви також можете використовувати CopyOnWriteArrayList замість ArrayList. Це останній рекомендований підхід від JDK 1.5 і далі.


1

У моєму випадку прийнята відповідь не працює, вона зупиняє виняток, але це викликає деяку непослідовність у моєму списку. Наступне рішення для мене прекрасно працює.

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

for (String value: list) {
   if (value.length() > 5) { // your condition
       itemsToRemove.add(value);
   }
}
list.removeAll(itemsToRemove);

У цей код я додав елементи для видалення, в інший список, а потім застосував list.removeAllметод для видалення всіх необхідних елементів.


0

"Я повинен спершу клонувати цей список?"

Це буде найпростішим рішенням, видаліть його з клону та скопіюйте клон назад після видалення.

Приклад з моєї гри rummikub:

SuppressWarnings("unchecked")
public void removeStones() {
  ArrayList<Stone> clone = (ArrayList<Stone>) stones.clone();
  // remove the stones moved to the table
  for (Stone stone : stones) {
      if (stone.isOnTable()) {
         clone.remove(stone);
      }
  }
  stones = (ArrayList<Stone>) clone.clone();
  sortStones();
}

2
Перш ніж звернутись до коментаря, учасники права повинні залишити коментар.
OneWorld

2
В цій відповіді немає нічого поганого, можливо, stones = (...) clone.clone();це зайве. Не stones = clone;зробили б те ж саме?
vikingsteve

Я погоджуюся, друге клонування непотрібне. Надалі ви можете спростити це, повторивши клон та видаливши елементи безпосередньо з stones. Таким чином, вам навіть не потрібна cloneзмінна: for (Stone stone : (ArrayList<Stone>) stones.clone()) {...
Zsolt Sky

0

Якщо ваша мета - вилучити всі елементи зі списку, ви можете переглядати кожен елемент, а потім зателефонувати:

list.clear()

0

Я приїжджаю пізно, я знаю, але відповідаю на це, тому що думаю, що це рішення є простим і елегантним:

List<String> listFixed = new ArrayList<String>();
List<String> dynamicList = new ArrayList<String>();

public void fillingList() {
    listFixed.add("Andrea");
    listFixed.add("Susana");
    listFixed.add("Oscar");
    listFixed.add("Valeria");
    listFixed.add("Kathy");
    listFixed.add("Laura");
    listFixed.add("Ana");
    listFixed.add("Becker");
    listFixed.add("Abraham");
    dynamicList.addAll(listFixed);
}

public void updatingListFixed() {
    for (String newList : dynamicList) {
        if (!listFixed.contains(newList)) {
            listFixed.add(newList);
        }
    }

    //this is for add elements if you want eraser also 

    String removeRegister="";
    for (String fixedList : listFixed) {
        if (!dynamicList.contains(fixedList)) {
            removeResgister = fixedList;
        }
    }
    fixedList.remove(removeRegister);
}

Все це для оновлення з одного списку в інший, і ви можете зробити все лише з одного списку, а в методі оновлення ви перевіряєте обидва списки і можете стирати або додавати елементи між списком. Це означає, що обидва списки завжди мають однаковий розмір


0

Використовуйте Iterator замість списку масивів

Потрібно перетворити набір в ітератор з відповідності типу

І переходимо до наступного елемента і видаляємо

Iterator<Insured> itr = insuredSet.iterator();
while (itr.hasNext()) { 
    itr.next();
    itr.remove();
}

Тут важливий перехід до наступного, оскільки він повинен приймати індекс для видалення елемента.


0

Що про

import java.util.Collections;

List<A> abc = Collections.synchronizedList(new ArrayList<>());

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.