Iterowanie przez kolekcję, unikanie ConcurrentModificationException podczas usuwania obiektów w pętli

1194

Wszyscy wiemy, że nie możesz wykonać następujących czynności z powodu ConcurrentModificationException:

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

Ale to najwyraźniej działa czasami, ale nie zawsze. Oto konkretny kod:

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

To oczywiście skutkuje:

Exception in thread "main" java.util.ConcurrentModificationException

Nawet jeśli wiele wątków tego nie robi. Tak czy siak.

Jakie jest najlepsze rozwiązanie tego problemu? Jak mogę usunąć element z kolekcji w pętli bez zgłaszania tego wyjątku?

Używam tu również arbitralnego Collection, niekoniecznie ArrayList, więc nie możesz na nim polegać get.

Claudiu
źródło
Uwaga dla czytelników: przeczytaj dokument docs.oracle.com/javase/tutorial/collections/interfaces/… , może to być łatwiejszy sposób na osiągnięcie tego, co chcesz zrobić.
GKFX

Odpowiedzi:

1601

Iterator.remove() jest bezpieczny, możesz go użyć w następujący sposób:

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

Pamiętaj, że Iterator.remove()jest to jedyny bezpieczny sposób modyfikowania kolekcji podczas iteracji; zachowanie nie jest określone, jeśli podstawowa kolekcja zostanie zmodyfikowana w jakikolwiek inny sposób podczas iteracji.

Źródło: docs.oracle> Interfejs kolekcji


I podobnie, jeśli masz ListIteratori chcesz dodać elementy, możesz użyć ListIterator#add, z tego samego powodu, którego możesz użyć Iterator#remove - jest tak zaprojektowane, aby na to pozwolić.


W twoim przypadku próbowałeś usunąć z listy, ale to samo ograniczenie obowiązuje, jeśli próbujesz przez putjakiś Mapczas iterować jego zawartość.

Bill K.
źródło
19
Co jeśli chcesz usunąć element inny niż element zwrócony w bieżącej iteracji?
Eugen
2
Musisz użyć .remove w iteratorze, który jest w stanie usunąć tylko bieżący element, więc nie :)
Bill K
1
Należy pamiętać, że jest to wolniejsze w porównaniu do korzystania z ConcurrentLinkedDeque lub CopyOnWriteArrayList (przynajmniej w moim przypadku)
Dan
1
Czy nie można umieścić iterator.next()połączenia w pętli for? Jeśli nie, czy ktoś może wyjaśnić, dlaczego?
Blake,
1
@ GonenI Jest zaimplementowany dla wszystkich iteratorów ze zbiorów, które nie są niezmienne. List.addjest w tym samym sensie „opcjonalny”, ale nie można powiedzieć, że dodanie do listy jest „niebezpieczne”.
Radiodef,
345

To działa:

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

Założyłem, że ponieważ pętla foreach to cukier syntaktyczny do iteracji, użycie iteratora nie pomogłoby ... ale daje tę .remove()funkcjonalność.

Claudiu
źródło
43
foreach loop to cukier syntaktyczny do iteracji. Jednak, jak wskazałeś, musisz wywołać opcję usuwania na iteratorze - do którego foreach nie daje dostępu. Stąd powód, dla którego nie można usunąć w pętli foreach (nawet jeśli rzeczywiście przy iterator pod maską)
madlep
36
+1 na przykład kod do użycia iter.remove () w kontekście, którego odpowiedź Billa K nie ma [bezpośrednio].
Ededified
202

W Javie 8 możesz użyć nowej removeIfmetody . Dotyczy twojego przykładu:

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

coll.removeIf(i -> i == 5);
assylias
źródło
3
Ooooo! Miałem nadzieję, że coś w Javie 8 lub 9 może pomóc. Nadal wydaje mi się to dość gadatliwe, ale nadal mi się podoba.
James T Snell,
Czy w tym przypadku zalecane jest także wdrożenie funkcji equals ()?
Anmol Gupta,
przy okazji removeIfużywa Iteratori whilepętli. Można to zobaczyć o godzinie 8 javajava.util.Collection.java
omerhakanbilici
3
@omerhakanbilici Niektóre implementacje, takie jak ArrayListprzesłaniają go ze względu na wydajność. Ten, o którym mówisz, jest tylko domyślną implementacją.
Didier L
@AnmolGupta: Nie, equalsnie jest tu w ogóle używany, więc nie trzeba go wdrażać. (Ale oczywiście, jeśli użyjesz equalsw teście, należy go zaimplementować tak, jak chcesz).
Lii,
42

Ponieważ na pytanie już udzielono odpowiedzi, tj. Najlepszym sposobem jest użycie metody remove obiektu iteratora, przejdę do szczegółów miejsca, w którym zgłaszany "java.util.ConcurrentModificationException"jest błąd .

Każda klasa posiada prywatny zbiór klasę, która implementuje interfejs Iterator i dostarcza metod, takich jak next(), remove()i hasNext().

Kod następnego wygląda mniej więcej tak ...

public E next() {
    checkForComodification();
    try {
        E next = get(cursor);
        lastRet = cursor++;
        return next;
    } catch(IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
    }
}

Tutaj metoda checkForComodificationjest implementowana jako

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

Jak widać, jeśli wyraźnie spróbujesz usunąć element z kolekcji. Powoduje to, modCountże różni się od expectedModCount, co powoduje wyjątek ConcurrentModificationException.

Ashish
źródło
Bardzo interesujące. Dziękuję Ci! Często sam nie nazywam remove (), zamiast tego wolę wyczyścić kolekcję po iteracji. Nie mówiąc już, że to dobry wzór, właśnie to robiłem ostatnio.
James T Snell,
26

Możesz albo użyć iteratora bezpośrednio jak wspomniano, albo zachować drugą kolekcję i dodać każdy element, który chcesz usunąć, do nowej kolekcji, a następnie usunąć wszystkie na końcu. Dzięki temu możesz nadal korzystać z bezpieczeństwa typu dla każdej pętli kosztem zwiększonego zużycia pamięci i czasu procesora (nie powinno to stanowić dużego problemu, chyba że masz naprawdę duże listy lub naprawdę stary komputer)

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);
}
RodeoClown
źródło
7
tak zwykle robię, ale jawny iterator jest bardziej wymyślnym rozwiązaniem.
Claudiu
1
W porządku, o ile nie robisz nic innego z iteratorem - ujawnienie go ułatwia wykonywanie takich czynności, jak wywołanie .next () dwa razy na pętlę itp. Nie jest to duży problem, ale może powodować problemy, jeśli robisz to coś bardziej skomplikowanego niż tylko przeglądanie listy w celu usunięcia wpisów.
RodeoClown,
@RodeoClown: w pierwotnym pytaniu Claudiu usuwa z kolekcji, a nie iteratora.
matt b
1
Usunięcie z iteratora usuwa z podstawowej kolekcji ... ale to, co powiedziałem w ostatnim komentarzu, to to, że jeśli robisz coś bardziej skomplikowanego niż po prostu usuwanie w pętli (np. Przetwarzanie poprawnych danych) za pomocą iteratora, możesz zrobić trochę błędy łatwiejsze do wykonania.
RodeoClown
Jeśli jest to proste usuwanie niepotrzebnych wartości, a pętla robi tylko to jedno, bezpośrednie użycie iteratora i wywołanie metody .remove () jest absolutnie w porządku.
RodeoClown
17

W takich przypadkach powszechną sztuczką jest (było?) Cofanie się:

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

To powiedziawszy, jestem bardziej niż szczęśliwy, że masz lepsze sposoby w Javie 8, np. removeIfLub filterw strumieniach.

Landei
źródło
2
To dobra sztuczka. Ale nie działałoby to na nieindeksowanych kolekcjach, takich jak zestawy, i byłoby naprawdę powolne na powiedzmy listach połączonych.
Claudiu
@Claudiu Tak, to zdecydowanie jest tylko dla ArrayLists lub podobnych kolekcji.
Landei
Korzystam z ArrayList, to działało idealnie, dzięki.
StarSweeper
2
indeksy są świetne. Jeśli to takie powszechne, dlaczego nie używasz for(int i = l.size(); i-->0;) {?
Jan
16

Taka sama odpowiedź jak Klaudiusz z pętlą for:

for (Iterator<Object> it = objects.iterator(); it.hasNext();) {
    Object object = it.next();
    if (test) {
        it.remove();
    }
}
Antzi
źródło
12

W przypadku kolekcji Eclipse metoda removeIfzdefiniowana w MutableCollection będzie działać:

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

W składni Lambda Java 8 można to zapisać w następujący sposób:

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

Wywołanie to Predicates.cast()jest tutaj konieczne, ponieważ removeIfw java.util.Collectioninterfejsie Java 8 została dodana domyślna metoda .

Uwaga: jestem osobą odpowiedzialną za kolekcje Eclipse .

Donald Raab
źródło
10

Utwórz kopię istniejącej listy i powtórz nową kopię.

for (String str : new ArrayList<String>(listOfStr))     
{
    listOfStr.remove(/* object reference or index */);
}
Priyank Doshi
źródło
19
Wykonywanie kopii brzmi jak marnotrawstwo zasobów.
Antzi
3
@Antzi To zależy od wielkości listy i gęstości obiektów. Wciąż cenne i ważne rozwiązanie.
mre
Korzystałem z tej metody. Wymaga nieco więcej zasobów, ale jest znacznie bardziej elastyczny i przejrzysty.
Tao Zhang,
Jest to dobre rozwiązanie, gdy nie masz zamiaru usuwać obiektów wewnątrz samej pętli, ale są one raczej „losowo” usuwane z innych wątków (np. Operacje sieciowe aktualizujące dane). Jeśli często robisz te kopie, jest nawet implementacja Java, która robi dokładnie to: docs.oracle.com/javase/8/docs/api/java/util/concurrent/…
A1m
8

Ludzie twierdzą, że nie można usunąć z kolekcji, która jest iterowana przez pętlę foreach. Chciałem tylko wskazać, że jest to technicznie niepoprawne i dokładnie opisać (wiem, że pytanie OP jest tak zaawansowane, że można uniknąć tego), kod stojący za tym założeniem:

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

Nie chodzi o to, że nie można usunąć iteracji, Colletiona raczej o to, że nie można kontynuować iteracji. Stąd breakw powyższym kodzie.

Przepraszam, jeśli ta odpowiedź jest nieco specjalistycznym przypadkiem użycia i bardziej pasuje do oryginalnego wątku, z którego tu przybyłem, ten jest oznaczony jako duplikat (pomimo tego, że ten wątek wydaje się bardziej niuansowy) tego i zablokowany.

Jan
źródło
8

Z tradycyjną pętlą for

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++;   
}
Lluis Felisart
źródło
Ach, więc tak naprawdę jest to tylko ulepszona pętla-for-która rzuca wyjątek.
cellepo
FWIW - ten sam kod nadal działałby po zmodyfikowaniu w celu zwiększenia i++ochrony pętli, a nie w treści pętli.
cellepo
Korekta ^: To znaczy, że i++zwiększenie nie było warunkowe - Rozumiem teraz, dlatego robisz to w ciele :)
cellepo
2

A ListIteratorpozwala dodawać lub usuwać elementy z listy. Załóżmy, że masz listę Carobiektów:

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);
   }
}
james.garriss
źródło
Dodatkowe metody w interfejsie ListIterator (rozszerzenie Iteratora) są interesujące - szczególnie jego previousmetoda.
cellepo
1

Mam sugestię dotyczącą powyższego problemu. Nie potrzeba dodatkowej listy ani dodatkowego czasu. Znajdź przykład, który zrobiłby to samo, ale w inny sposób.

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

Pozwoliłoby to uniknąć wyjątku dotyczącego współbieżności.

Nandhan Thiravia
źródło
1
Pytanie wyraźnie stwierdza, że ​​PO nie jest konieczne, ArrayLista zatem nie można na nim polegać get(). W przeciwnym razie prawdopodobnie dobre podejście.
kaskelotti
(Wyjaśnienie ^) OP używa arbitralnie Collection- Collectioninterfejs nie obejmuje get. (Chociaż Listinterfejs FWIW zawiera „get”).
cellepo
Właśnie dodałem tutaj osobną, bardziej szczegółową odpowiedź również dotyczącą whilezapętlania List. Ale +1 dla tej odpowiedzi, ponieważ była pierwsza.
cellepo
1

ConcurrentHashMap lub ConcurrentLinkedQueue lub ConcurrentSkipListMap może być inną opcją, ponieważ nigdy nie zgłoszą żadnego wyjątku ConcurrentModificationExap, nawet jeśli usuniesz lub dodasz element.

Tak
źródło
Tak, i pamiętaj, że wszystkie są w java.util.concurrentpakiecie. Niektóre inne podobne / wspólne klasy przypadków użycia z tego pakietu to CopyOnWriteArrayList & CopyOnWriteArraySet [ale nie tylko].
cellepo
Właściwie właśnie się dowiedziałem, że chociaż obiektów struktury danych unikają ConcurrentModificationException , używanie ich w rozszerzonej pętli -for może nadal powodować problemy z indeksowaniem (tj. Nadal pomijanie elementów lubIndexOutOfBoundsException
-for
1

Wiem, że to pytanie jest zbyt stare, aby dotyczyć Javy 8, ale dla tych, którzy używają Javy 8, możesz łatwo użyć 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);
Pedram Bashiri
źródło
1

Innym sposobem jest utworzenie kopii tablicy arrayList:

List<Object> l = ...

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

for (Object i : iterationList) {
    if (condition(i)) {
        l.remove(i);
    }
}
Nestor Milyaev
źródło
Uwaga: inie jest obiektem typu „ indexale”. Może zadzwonienie objbyłoby bardziej odpowiednie.
luckydonald
1

Najlepszym (zalecanym) sposobem jest użycie pakietu java.util.Concurrent. Korzystając z tego pakietu, możesz łatwo uniknąć tego wyjątku. odnoszą się do Zmodyfikowanego Kodu

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);
}
jagdish khetre
źródło
0

W przypadku ArrayList: remove (int index) - jeśli (indeks jest pozycją ostatniego elementu) unika się go bez System.arraycopy()i nie zajmuje to czasu.

czas arraycopy wydłuża się, gdy (indeks maleje), przy okazji maleje także elementy listy!

najlepszym skutecznym sposobem usuwania jest usuwanie jej elementów w kolejności malejącej: while(list.size()>0)list.remove(list.size()-1);// przyjmuje O (1) while(list.size()>0)list.remove(0);// przyjmuje O (silnia (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
  • dla pętli indeksu: 1090 ms
  • dla indeksu desc: 519 ms --- najlepszy
  • dla iteratora: 1043 ms
Nurlan
źródło
0
for (Integer i : l)
{
    if (i.intValue() == 5){
            itemsToRemove.add(i);
            break;
    }
}

Złap to po usunięciu elementu z listy, jeśli pominiesz wewnętrzne wywołanie iterator.next (). wciąż działa! Chociaż nie proponuję pisać takiego kodu, pomaga zrozumieć koncepcję :-)

Twoje zdrowie!

Srinivasan Thoyyeti
źródło
0

Przykład modyfikacji bezpiecznego gromadzenia wątków:

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();
            }
        }
    }
}
Yazon2006
źródło
0

Wiem, że to pytanie zakłada tylko Collection, a dokładniej żadne List. Ale dla tych, którzy czytają to pytanie, którzy faktycznie pracują z Listreferencją, możesz uniknąć ConcurrentModificationExceptionza pomocą whilepętli (podczas modyfikowania w niej), jeśli chcesz uniknąćIterator (albo jeśli chcesz uniknąć w ogóle, albo unikaj go konkretnie, aby osiągnąć kolejność zapętlania różni się od zatrzymania od początku do końca przy każdym elemencie [co, jak sądzę, jest jedynym, który Iteratorsam może wykonać]):

* Aktualizacja: patrz komentarze poniżej wyjaśniające, że analogia jest również możliwa do osiągnięcia dzięki tradycyjnej pętli-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;
    }
}

Brak ConcurrentModificationException od tego kodu.

Widzimy tam, że zapętlenie nie zaczyna się od początku i nie zatrzymuje się na każdym elemencie (w co wierzę)Iterator nie jest w stanie zrobić).

FWIW również widzimy, getże jest wywoływany list, czego nie można byłoby zrobić, gdyby jego odwołanie było po prostu Collection(zamiast bardziej specyficznego Listtypu Collection) - Listinterfejs zawiera get, ale Collectioninterfejs nie. Gdyby nie ta różnica, wówczas listodniesieniem może być Collection[i dlatego technicznie ta odpowiedź byłaby wówczas odpowiedzią bezpośrednią, zamiast odpowiedzi stycznej].

FWIWW ten sam kod nadal działa po zmodyfikowaniu, aby zaczynał od początku na stopie przy każdym elemencie (podobnie jak Iteratorkolejność):

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;
    }
}
cellepo
źródło
Jednak nadal wymaga to bardzo starannego obliczenia wskaźników do usunięcia.
OneCricketeer,
Jest to także bardziej szczegółowe wyjaśnienie tej odpowiedzi stackoverflow.com/a/43441822/2308683
OneCricketeer
Dobrze wiedzieć - dzięki! Że inna odpowiedź pomogła mi zrozumieć, że jest to wzmocniona -dla pętli że rzucał ConcurrentModificationException, ale nie tradycyjny -dla-loop (których inne zastosowania odpowiedź) - nie zdając sobie sprawy, że przed dlatego motywowane było napisać tę odpowiedź (ja mylnie pomyślał wtedy, że to wszystko zapętli wyjątek).
cellepo
0

Jednym z rozwiązań mogłoby być obrócenie listy i usunięcie pierwszego elementu, aby uniknąć ConcurrentModificationException lub 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);
Rahul Vala
źródło
0

Spróbuj tego (usuwa wszystkie elementy z listy, które są równe i):

for (Object i : l) {
    if (condition(i)) {
        l = (l.stream().filter((a) -> a != i)).collect(Collectors.toList());
    }
}
Oleg Tatarchuk
źródło
-2

może to nie być najlepszy sposób, ale w większości małych przypadków powinno to być dopuszczalne:

„utwórz drugą pustą tablicę i dodaj tylko te, które chcesz zachować”

Nie pamiętam, skąd to przeczytałem ... dla słuszności zrobię tę wiki w nadziei, że ktoś ją znajdzie lub po prostu nie zarobię przedstawiciela, na który nie zasługuję.

ajax333221
źródło