Jak uniknąć wyjątku java.util.ConcurrentModificationException podczas iteracji i usuwania elementów z ArrayList


203

Mam ArrayList, który chcę iterować. Podczas iteracji po nim muszę jednocześnie usuwać elementy. Oczywiście rzuca tojava.util.ConcurrentModificationException .

Jaka jest najlepsza praktyka radzenia sobie z tym problemem? Czy powinienem najpierw sklonować listę?

Usuwam elementy nie z samej pętli, ale z innej części kodu.

Mój kod wygląda następująco:

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.doSomethingmoże zadzwonić Test.removeA();


Odpowiedzi:


325

Dwie opcje:

  • Utwórz listę wartości, które chcesz usunąć, dodając do tej listy w pętli, a następnie wywołaj originalList.removeAll(valuesToRemove) na końcu
  • Użyj remove()metody na samym iteratorze. Pamiętaj, że oznacza to, że nie możesz używać rozszerzonej pętli for.

Jako przykład drugiej opcji, usunięcie z listy ciągów o długości większej niż 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
Powinienem wspomnieć, że usuwam elementy z innej części kodu, a nie z samej pętli.
RoflcoptrException

@Roflcoptr: Trudno odpowiedzieć bez zobaczenia, jak oddziałują dwa bity kodu. Zasadniczo nie możesz tego zrobić. Nie jest oczywiste, czy najpierw klonowanie listy pomogłoby, nie widząc, jak wszystko się wisi. Czy możesz podać więcej szczegółów w swoim pytaniu?
Jon Skeet,

Wiem, że klonowanie listy pomogłoby, ale nie wiem, czy to dobre podejście. Ale dodam trochę kodu.
RoflcoptrException

2
To rozwiązanie prowadzi również do wyjątku java.util.ConcurrentModificationException, patrz stackoverflow.com/a/18448699/2914140 .
CoolMind

1
@CoolMind: Bez wielu wątków ten kod powinien być w porządku.
Jon Skeet

17

Z JavaDocs z ArrayList

Iteratory zwrócone przez iterator tej klasy i metody listIterator działają szybko: jeśli lista jest modyfikowana strukturalnie w dowolnym momencie po utworzeniu iteratora, w jakikolwiek sposób, z wyjątkiem własnych metod usuwania lub dodawania iteratora, iterator zgłosi wyjątek ConcurrentModificationException.


6
i gdzie jest odpowiedź na pytanie?
Adelin

Tak jak mówi, z wyjątkiem własnych metod usuwania lub dodawania iteratora
Varun Achar

14

Próbujesz usunąć wartość z listy w zaawansowanej pętli for, co nie jest możliwe, nawet jeśli zastosujesz jakąś sztuczkę (co zrobiłeś w kodzie). Lepszym sposobem jest kodowanie poziomu iteratora, zgodnie z zaleceniami innych tutaj.

Zastanawiam się, jak ludzie nie sugerowali tradycyjnego podejścia do pętli.

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

To też działa.


2
To nie jest poprawne!!! kiedy usuwasz element, następny zajmuje jego pozycję i podczas gdy ja zwiększa, następny element nie jest sprawdzany w następnej iteracji. W takim przypadku powinieneś wybrać (int i = lStringList.size (); i> -1; i--)
Johntor

1
Zgodzić się! Alternatywą jest wykonanie i--; in, jeśli warunek w pętli for.
suhas0sn07

Myślę, że ta odpowiedź została zredagowana w celu rozwiązania problemów w powyższych komentarzach, więc teraz jest w porządku, przynajmniej dla mnie.
Kira Resari

11

Naprawdę powinieneś po prostu powtórzyć tablicę w tradycyjny sposób

Za każdym razem, gdy usuniesz element z listy, elementy po nim zostaną przesunięte do przodu. Dopóki nie zmienisz elementów innych niż iteracyjny, poniższy kod powinien działać.

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

W Javie 8 możesz użyć interfejsu Collection i zrobić to, wywołując metodę removeIf:

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

Więcej informacji można znaleźć tutaj


6

Wykonaj pętlę w normalny sposób java.util.ConcurrentModificationException jest to błąd związany z dostępnymi elementami.

Więc spróbuj:

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

Uniknąłeś tego java.util.ConcurrentModificationException, nie usuwając niczego z listy. Zdradliwy. :) Naprawdę nie można nazwać tego „normalnym sposobem” w celu iteracji listy.
Zsolt Sky

6

Podczas iteracji listy można usunąć element. Zobaczmy poniżej moje przykłady,

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

Mam powyższe nazwy listy Array. I chcę usunąć nazwę „def” z powyższej listy,

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

Powyższy kod zgłasza ConcurrentModificationException wyjątek ponieważ modyfikujesz listę podczas iteracji.

Tak więc, aby usunąć nazwę „def” z Arraylist, postępując w ten sposób,

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

Powyższy kod, za pomocą iteratora możemy usunąć nazwę „def” z Arraylist i spróbować wydrukować tablicę, zobaczysz poniższe wyjście.

Dane wyjściowe: [abc, ghi, xyz]


W przeciwnym razie możemy użyć listy współbieżnej, która jest dostępna w pakiecie współbieżnym, abyś mógł wykonywać operacje usuwania i dodawania podczas iteracji. Na przykład zobacz poniższy fragment kodu. ArrayList <String> names = new ArrayList <String> (); CopyOnWriteArrayList <String> copyNames = new CopyOnWriteArrayList <String> (names); for (Nazwa ciągu: copyNames) {if (name.equals („def”)) {copyNames.remove („def”); }}
Indra K

CopyOnWriteArrayList będzie najbardziej kosztownymi operacjami.
Indra K

5

Jedną z opcji jest zmodyfikowanie removeAmetody w tym celu -

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

Ale oznaczałoby to Twój doSomething()powinien być w stanie przekazać iteratordoremove metody. Niezbyt dobry pomysł.

Czy możesz to zrobić w podejściu dwuetapowym: w pierwszej pętli, gdy iterujesz listę, zamiast usuwać wybrane elementy, zaznacz je jako do usunięcia . W tym celu możesz po prostu skopiować te elementy (płytka kopia) do innegoList .

Następnie, po zakończeniu iteracji, po prostu zrób removeAllz pierwszej listy wszystkie elementy na drugiej liście.


Doskonale, zastosowałem to samo podejście, chociaż zapętliłem dwa razy. sprawia, że ​​wszystko jest proste i nie ma z tym równoczesnych problemów :)
Pankaj Nimgade

1
Nie widzę, aby Iterator miał metodę remove (a). Metoda remove () nie przyjmuje argumentów docs.oracle.com/javase/8/docs/api/java/util/Iterator.html czego mi brakuje?
c0der

5

Oto przykład, w którym używam innej listy, aby dodać obiekty do usunięcia, a następnie używam stream.foreach, aby usunąć elementy z oryginalnej listy:

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

Myślę, że wykonujesz dodatkową pracę wykonując dwie pętle, w najgorszym przypadku pętle byłyby z całej listy. Byłoby to najprostsze i tańsze wykonanie tylko w jednej pętli.
Luis Carlos,

Nie sądzę, że możesz usunąć obiekt z pierwszej pętli, stąd potrzeba dodatkowej pętli usuwającej, również pętla usuwająca to tylko obiekty do usunięcia - być może mógłbyś napisać przykład z tylko jedną pętlą, chciałbym to zobaczyć - dzięki @ LuisCarlos
serup

Jak mówisz za pomocą tego kodu, nie możesz usunąć żadnego elementu wewnątrz pętli for, ponieważ powoduje to wyjątek java.util.ConcurrentModificationException. Jednak możesz użyć podstawowego dla. Tutaj piszę przykład z wykorzystaniem części twojego kodu.
Luis Carlos

1
for (int i = 0; i <customerTableViewItems.size (); i ++) {diff = currentTimestamp.getValue (). getTime () - klienciTableViewItems.get (i) .timestamp.getValue (). getTime (); diffSeconds = diff / 1000% 60; if (diffSeconds> 10) {customerTableViewItems.remove (i--); }} Jest ważne i-- ponieważ nie chcesz pominąć żadnego elementu. Możesz także użyć metody removeIf (filtr Predicate <? Super E>) dostarczonej przez klasę ArrayList. Mam nadzieję, że ta pomoc
Luis Carlos

1
Wyjątek występuje, ponieważ w pętli for istnieje jako aktywne odwołanie do iteratora listy. W normalnym przypadku nie ma odniesienia i masz większą elastyczność w zakresie zmiany danych. Mam nadzieję, że ta pomoc
Luis Carlos

3

Zamiast używać Dla każdej pętli użyj normal dla pętli. na przykład poniższy kod usuwa wszystkie elementy z listy tablic bez podania wyjątku java.util.ConcurrentModificationExod. Możesz zmodyfikować warunek w pętli zgodnie z przypadkiem użycia.

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

          e.remove(i);
        }

2

Zrób coś takiego prostego:

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

2

Alternatywne rozwiązanie Java 8 wykorzystujące strumień:

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

W Javie 7 możesz zamiast tego używać Guava:

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

Zauważ, że przykład Guawy daje niezmienną listę, która może, ale nie musi być tym, czego chcesz.


1

Możesz także użyć CopyOnWriteArrayList zamiast ArrayList. Jest to najnowsze zalecane podejście od JDK 1.5 i nowsze.


1

W moim przypadku zaakceptowana odpowiedź nie działa, zatrzymuje wyjątek, ale powoduje pewną niespójność na mojej liście. Poniższe rozwiązanie doskonale dla mnie działa.

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

W tym kodzie dodałem elementy do usunięcia, na innej liście, a następnie zastosowałem list.removeAllmetodę usunięcia wszystkich wymaganych elementów.


0

„Czy powinienem najpierw sklonować listę?”

To będzie najłatwiejsze rozwiązanie, usuń z klonu i skopiuj klon z powrotem po usunięciu.

Przykład z mojej gry 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
Downvoters powinien przynajmniej zostawić komentarz przed oddaniem głosu.
OneWorld,

2
Z tą odpowiedzią nie ma nic złego, można się spodziewać, że stones = (...) clone.clone();jest to zbyteczne. Czy nie stones = clone;zrobiłby tego samego?
vikingsteve

Zgadzam się, drugie klonowanie jest niepotrzebne. Możesz to jeszcze uprościć, wykonując iterację na klonie i usuwając elementy bezpośrednio z stones. W ten sposób nie potrzebujesz nawet clonezmiennej: for (Stone stone : (ArrayList<Stone>) stones.clone()) {...
Zsolt Sky

0

Jeśli Twoim celem jest usunięcie wszystkich elementów z listy, możesz iterować każdy element, a następnie wywołać:

list.clear()

0

Przyjeżdżam późno, wiem, ale odpowiadam na to, ponieważ uważam, że to rozwiązanie jest proste i eleganckie:

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

Wszystko to służy do aktualizacji z jednej listy do drugiej i możesz zrobić wszystko z tylko jednej listy, a przy aktualizacji metody sprawdzasz obie listy i możesz wymazać lub dodać elementy między listami. Oznacza to, że obie listy zawsze mają ten sam rozmiar


0

Użyj Iteratora zamiast listy macierzy

Niech zestaw zostanie przekonwertowany na iterator z dopasowaniem typu

I przejdź do następnego elementu i usuń

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

Przejście do następnego jest tutaj ważne, ponieważ powinien zająć indeks, aby usunąć element.


0

A co z

import java.util.Collections;

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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.