Ітерація через колекцію, уникаючи 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 в ітераторі, і це зможе видалити лише поточний елемент, так що ні :)
Білл К

1
Будьте в курсі, що це повільніше порівняно з використанням ConcurrentLinkedDeque або CopyOnWriteArrayList (принаймні в моєму випадку)
Dan

1
Чи не можливо поставити iterator.next()дзвінок у фор-цикл? Якщо ні, то хтось може пояснити, чому?
Блейк

1
@GonenI Він реалізований для всіх ітераторів із колекцій, які не змінюються. List.addтакож є "необов'язковим" у тому ж сенсі, але ви не сказали б, що це "небезпечно" додавати до списку.
Radiodef

345

Це працює:

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

Я припускав, що оскільки цикл foreach є синтаксичним цукром для ітерації, використання ітератора не допоможе ... але це дає вам цю .remove()функціональність.


43
петля foreach - це синтаксичний цукор для ітерації. Однак, як ви вказали, вам потрібно зателефонувати видалити на ітераторі - до якого 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 може допомогти. Це все ще здається мені досить багатослівним, але мені все одно подобається.
Джеймс Т Снелл

Чи рекомендується реалізація рівних () і в цьому випадку?
Анмол Гупта

до речі removeIfвикористовує Iteratorі whileпетлю. Ви можете побачити це на java 8java.util.Collection.java
omerhakanbilici

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.


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

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 чи подібних колекцій.
Ландей

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

Примітка. Я є членом колекції Eclipse .


10

Зробіть копію наявного списку та повторіть нову копію.

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

19
Створення копії звучить як трата ресурсів.
Анци

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

Так, це дійсно просто розширений цикл, який кидає виняток.
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(). Інакше, мабуть, хороший підхід, хоча.
kaskelotti

(Роз'яснення ^) ОП використовується довільно Collection- Collectionінтерфейс не включає get. (Хоча Listінтерфейс FWIW включає "отримати").
cellepo

Я щойно додав окремий, більш детальний відповідь тут також для while-looping a List. Але +1 за цю відповідь, тому що вона прийшла першою.
cellepo

1

ConcurrentHashMap або ConcurrentLinkedQueue або ConcurrentSkipListMap може бути іншим варіантом, тому що вони ніколи не кинуть жодне ConcurrentModificationException, навіть якщо ви видалите або додаєте елемент.


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

Насправді я щойно дізнався, що хоча ці об’єкти даних об'єктів уникають ConcurrentModificationException , використання їх у посиленому циклі все-таки може спричинити проблеми індексації (тобто: все ще пропускають елементи, або IndexOutOfBoundsException...)
cellepo

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: delete (int index) - if (індекс є позицією останнього елемента), він уникає без 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 мсек

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-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-типу 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, але НЕ традиційний -дль петля (які інші відповідь використання) - не розуміючи , що перед тим , як , чому я був мотивований , щоб написати цю відповідь (я помилково тоді думав, що це все для-циклів, що кине виняток).
віолончель

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

-2

це може бути не найкращим способом, але для більшості невеликих випадків це повинно бути прийнятним:

"створити другий порожній масив і додати лише ті, які ви хочете зберегти"

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

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