Scorrendo una raccolta, evitando ConcurrentModificationException durante la rimozione di oggetti in un ciclo


1194

Sappiamo tutti che non puoi fare quanto segue a causa di ConcurrentModificationException:

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

Ma a quanto pare a volte funziona, ma non sempre. Ecco un codice specifico:

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

Questo, ovviamente, si traduce in:

Exception in thread "main" java.util.ConcurrentModificationException

Anche se più thread non lo stanno facendo. Comunque.

Qual è la migliore soluzione a questo problema? Come posso rimuovere un elemento dalla raccolta in un ciclo senza generare questa eccezione?

Sto anche usando un arbitrario Collectionqui, non necessariamente un ArrayList, quindi non puoi fare affidamento get.


Nota per i lettori: leggi docs.oracle.com/javase/tutorial/collections/interfaces/… , potrebbe avere un modo più semplice per ottenere ciò che vuoi fare.
GKFX,

Risposte:


1601

Iterator.remove() è sicuro, puoi usarlo in questo modo:

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

Si noti che Iterator.remove()è l'unico modo sicuro per modificare una raccolta durante l'iterazione; il comportamento non è specificato se la raccolta sottostante viene modificata in qualsiasi altro modo mentre è in corso l'iterazione.

Fonte: docs.oracle> L'interfaccia della raccolta


Allo stesso modo, se si dispone di un ListIteratore si desidera aggiungere elementi, è possibile utilizzare ListIterator#add, per lo stesso motivo per cui è possibile utilizzare Iterator#remove : è progettato per consentirlo.


Nel tuo caso hai provato a rimuovere da un elenco, ma si applica la stessa restrizione se si tenta di ripetere putun Mappo 'il suo contenuto.


19
Cosa succede se si desidera rimuovere un elemento diverso dall'elemento restituito nell'iterazione corrente?
Eugen,

2
Devi usare .remove nell'iteratore e questo è solo in grado di rimuovere l'elemento corrente, quindi no :)
Bill K

1
Tieni presente che questo è più lento rispetto all'utilizzo di ConcurrentLinkedDeque o CopyOnWriteArrayList (almeno nel mio caso)
Dan

1
Non è possibile mettere la iterator.next()chiamata nel for-loop? In caso contrario, qualcuno può spiegare perché?
Blake,

1
@GonenI È implementato per tutti gli iteratori da raccolte che non sono immutabili. List.addè "opzionale" anche nello stesso senso, ma non diresti che è "non sicuro" aggiungere a un elenco.
Radiodef,

345

Questo funziona:

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

Ho pensato che dato che un ciclo foreach è uno zucchero sintattico per l'iterazione, l'uso di un iteratore non sarebbe d'aiuto ... ma ti dà questa .remove()funzionalità.


43
foreach loop è lo zucchero sintattico per iterare. Tuttavia, come hai sottolineato, devi chiamare remove sull'iteratore - a cui foreach non ti dà accesso. Da qui il motivo per cui non è possibile rimuovere in un ciclo foreach (anche se in realtà si sta utilizzando un iteratore sotto il cofano)
madlep,

36
+1 per esempio codice per usare iter.remove () nel contesto, che la risposta di Bill K non ha [direttamente].
Eddificato il

202

Con Java 8 è possibile utilizzare il nuovo removeIfmetodo . Applicato al tuo esempio:

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

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

3
Ooooo! Speravo che qualcosa in Java 8 o 9 potesse aiutare. Mi sembra ancora piuttosto prolisso, ma mi piace ancora.
James T Snell,

Anche l'implementazione equals () è consigliata in questo caso?
Anmol Gupta,

a proposito removeIfusa Iteratore whileloop. Puoi vederlo a java 8java.util.Collection.java
omerhakanbilici

3
@omerhakanbilici Alcune implementazioni come la ArrayListsovrascrivono per motivi di prestazioni. Quello a cui ti riferisci è solo l'implementazione predefinita.
Didier L

@AnmolGupta: No, qui equalsnon viene utilizzato affatto, quindi non deve essere implementato. (Ma ovviamente, se lo usi equalsnel tuo test, allora deve essere implementato nel modo desiderato.)
Lii

42

Poiché alla domanda è già stata data una risposta, ovvero il modo migliore è utilizzare il metodo di rimozione dell'oggetto iteratore, andrei nei dettagli del luogo in cui "java.util.ConcurrentModificationException"viene generato l'errore .

Ogni classe di raccolta ha una classe privata che implementa l'interfaccia Iterator e fornisce metodi come next(), remove()e hasNext().

Il codice per il prossimo assomiglia a questo ...

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

Qui il metodo checkForComodificationè implementato come

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

Quindi, come puoi vedere, se provi esplicitamente a rimuovere un elemento dalla raccolta. Ne risulta una modCountdifferenza expectedModCount, con conseguente eccezione ConcurrentModificationException.


Molto interessante. Grazie! Spesso non chiamo remove () da solo, preferisco invece cancellare la raccolta dopo averla ripetuta. Per non dire che è un buon modello, proprio quello che ho fatto di recente.
James T Snell,

26

Puoi utilizzare l'iteratore direttamente come hai menzionato oppure conservare una seconda raccolta e aggiungere tutti gli elementi che desideri rimuovere alla nuova raccolta, quindi rimuovere Tutti alla fine. Ciò ti consente di continuare a utilizzare la sicurezza del tipo per ogni ciclo al costo di un maggiore utilizzo della memoria e del tempo della CPU (non dovrebbe essere un grosso problema a meno che tu non abbia elenchi molto, molto grandi o un computer molto vecchio)

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
questo è ciò che faccio normalmente, ma l'iteratore esplicito è una soluzione più elgante che sento.
Claudiu,

1
Abbastanza giusto, fintanto che non stai facendo nient'altro con l'iteratore - averlo esposto rende più facile fare cose come call .next () due volte per loop ecc. Non è un grosso problema, ma può causare problemi se lo stai facendo niente di più complicato che semplicemente scorrere un elenco per eliminare le voci.
RodeoClown,

@RodeoClown: nella domanda originale, Claudiu sta rimuovendo dalla Collezione, non dall'iteratore.
opaco b

1
La rimozione dall'iteratore rimuove dalla raccolta sottostante ... ma quello che stavo dicendo nell'ultimo commento è che se stai facendo qualcosa di più complicato della semplice ricerca di eliminazioni nel ciclo (come l'elaborazione di dati corretti) utilizzando l'iteratore puoi fare un po ' errori più facili da fare.
RodeoClown

Se si tratta di un semplice valore di eliminazione che non è necessario e il ciclo sta facendo solo quella cosa, usare direttamente l'iteratore e chiamare .remove () va assolutamente bene.
RodeoClown

17

In questi casi un trucco comune è (era?) Per tornare indietro:

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

Detto questo, sono più che felice di avere modi migliori in Java 8, ad esempio removeIfo filtersu stream.


2
Questo è un buon trucco. Ma non funzionerebbe su raccolte non indicizzate come set, e sarebbe molto lento nel dire elenchi collegati.
Claudiu,

@Claudiu Sì, questo è sicuramente solo per ArrayListcollezioni simili.
Landei,

Sto usando un ArrayList, questo ha funzionato perfettamente, grazie.
StarSweeper,

2
gli indici sono fantastici. Se è così comune, perché non usi for(int i = l.size(); i-->0;) {?
Giovanni,

16

Stessa risposta di Claudio con un ciclo for:

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

12

Con Eclipse Collections , il metodo removeIfdefinito su MutableCollection funzionerà:

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

Con la sintassi Lambda Java 8 questo può essere scritto come segue:

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

La chiamata a Predicates.cast()è necessaria qui perché removeIfè stato aggiunto un metodo predefinito java.util.Collectionsull'interfaccia in Java 8.

Nota: sono un committer per le raccolte Eclipse .


10

Creare una copia dell'elenco esistente e scorrere su una nuova copia.

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

19
Fare una copia sembra uno spreco di risorse.
Antzi,

3
@Antzi Dipende dalle dimensioni dell'elenco e dalla densità degli oggetti all'interno. Ancora una soluzione preziosa e valida.
martedì

Ho usato questo metodo. Ci vuole un po 'più di risorse, ma molto più flessibile e chiaro.
Tao Zhang,

Questa è una buona soluzione quando non si intende rimuovere oggetti all'interno del ciclo stesso, ma vengono piuttosto "casualmente" rimossi da altri thread (ad es. Operazioni di rete che aggiornano i dati). Se vi trovate a fare queste copie molto c'è anche un'implementazione Java facendo esattamente questo: docs.oracle.com/javase/8/docs/api/java/util/concurrent/...
A1m

8

Le persone affermano che non è possibile rimuovere da una raccolta ripetuta da un ciclo foreach. Volevo solo sottolineare che è tecnicamente scorretto e descrivere esattamente (so che la domanda del PO è così avanzata da evitare di conoscerlo) il codice alla base di tale presupposto:

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

Non è che non è possibile rimuovere dall'iterato Colletionpiuttosto che non è possibile continuare l'iterazione una volta eseguita. Da qui il breaknel codice sopra.

Ci scusiamo se questa risposta è un caso d'uso un po 'specialistico e più adatto al thread originale da cui sono arrivato qui, che uno è contrassegnato come duplicato (nonostante questo thread appaia più sfumato) di questo e bloccato.


8

Con un ciclo tradizionale per

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

Ah, quindi è davvero solo il potenziato -per-loop che genera l'eccezione.
cellepo,

FWIW: lo stesso codice funzionerebbe comunque dopo essere stato modificato per aumentare i++nella protezione del loop anziché all'interno del corpo del loop.
cellepo,

Correzione ^: Questo è se l' i++incremento non fosse condizionale - vedo ora è per questo che lo fai nel corpo :)
cellepo

2

A ListIteratorconsente di aggiungere o rimuovere elementi nell'elenco. Supponiamo di avere un elenco di Caroggetti:

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

I metodi aggiuntivi nell'interfaccia ListIterator (estensione di Iterator) sono interessanti, in particolare il suo previousmetodo.
cellepo,

1

Ho un suggerimento per il problema sopra. Non è necessario un elenco secondario o alcun tempo aggiuntivo. Ecco un esempio che farebbe le stesse cose ma in modo diverso.

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

Ciò eviterebbe l'eccezione di concorrenza.


1
La domanda afferma esplicitamente che il PO non è necessario utilizzando ArrayListe quindi non può fare affidamento get(). Altrimenti probabilmente un buon approccio, comunque.
Kaskelotti,

(Chiarimento ^) OP utilizza un'interfaccia arbitraria Collection- l' Collectioninterfaccia non include get. (Sebbene l' Listinterfaccia FWIW includa 'get').
cellepo,

Ho appena aggiunto una risposta separata e più dettagliata qui anche per while-looping a List. Ma +1 per questa risposta perché è arrivata prima.
cellepo,

1

ConcurrentHashMap o ConcurrentLinkedQueue o ConcurrentSkipListMap possono essere un'altra opzione, perché non genereranno mai ConcurrentModificationException, anche se si rimuove o si aggiunge un elemento.


Sì, e nota che quelli sono tutti nel java.util.concurrentpacchetto. Alcune altre classi di casi simili / di uso comune di quel pacchetto sono CopyOnWriteArrayList & CopyOnWriteArraySet [ma non limitate a quelle].
cellepo,

In realtà, ho appena appreso che, sebbene tali strutture di dati evitino gli oggetti ConcurrentModificationException, usarli in un ciclo avanzato per-loop può comunque causare problemi di indicizzazione (ad esempio: saltare ancora gli elementi o IndexOutOfBoundsException...)
cellepo

1

So che questa domanda è troppo vecchia per essere su Java 8, ma per quelli che usano Java 8 puoi facilmente usare 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

Un altro modo è quello di creare una copia del tuo arrayList:

List<Object> l = ...

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

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

Nota: inon è un indexma invece l'oggetto. Forse chiamarlo objsarebbe più appropriato.
luckydonald

1

Il modo migliore (consigliato) è l'uso del pacchetto java.util.Concurrent. Usando questo pacchetto puoi facilmente evitare questa eccezione. fare riferimento al codice modificato

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

Nel caso ArrayList: remove (int index) - se (index è la posizione dell'ultimo elemento) evita senza System.arraycopy()e non richiede tempo per questo.

il tempo di array aumenta se (l'indice diminuisce), tra l'altro anche gli elementi dell'elenco diminuiscono!

il modo più efficace per rimuovere è rimuovere i suoi elementi in ordine decrescente: while(list.size()>0)list.remove(list.size()-1);// prende O (1) while(list.size()>0)list.remove(0);// prende O (fattoriale (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
  • per loop indice: 1090 msec
  • per indice desc: 519 msec --- il migliore
  • per iteratore: 1043 msec

0
for (Integer i : l)
{
    if (i.intValue() == 5){
            itemsToRemove.add(i);
            break;
    }
}

Il problema è che dopo aver rimosso l'elemento dall'elenco se si salta la chiamata interna iterator.next (). funziona ancora! Anche se non propongo di scrivere codice in questo modo, aiuta a capire il concetto alla base :-)

Saluti!


0

Esempio di modifica della raccolta sicura dei thread:

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

So che questa domanda presuppone solo una Collection, e non più specificamente alcuna List. Ma per coloro che leggono questa domanda che stanno effettivamente lavorando con un Listriferimento, è possibile evitare ConcurrentModificationExceptioncon un while-loop (mentre si modifica all'interno di esso) invece se si desidera evitareIterator (o se si desidera evitarlo in generale, o evitarlo specificamente per raggiungere un ordine di loop diverso dall'interruzione dall'inizio alla fine di ciascun elemento [che credo sia l'unico ordineIterator può fare]]:

* Aggiornamento: vedere i commenti seguenti che chiariscono che l'analogo è realizzabile anche con il tradizionale -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;
    }
}

Nessuna ConcurrentModificationException da quel codice.

Lì vediamo che il looping non inizia all'inizio e non si ferma ad ogni elemento (che credoIterator non possa fare da solo).

FWIW vediamo anche che getviene chiamato list, il che non potrebbe essere fatto se il suo riferimento fosse giusto Collection(invece del Listtipo più specifico di Collection) -List interfaccia include get, ma l' Collectioninterfaccia no. Se non fosse per quella differenza, allora il listriferimento potrebbe invece essere un Collection[e quindi tecnicamente questa risposta sarebbe quindi una risposta diretta, anziché una risposta tangenziale].

FWIWW lo stesso codice funziona ancora dopo la modifica per iniziare all'inizio alla fermata in ogni elemento (proprio come l' Iteratorordine):

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

Ciò richiede comunque un attento calcolo delle indicazioni da rimuovere.
OneCricketeer il

Inoltre, questa è solo una spiegazione più dettagliata di questa risposta stackoverflow.com/a/43441822/2308683
OneCricketeer

Buono a sapersi - grazie! Quell'altra risposta mi ha aiutato a capire che sarebbe il potenziato -per-loop che avrebbe lanciato ConcurrentModificationException, ma non il tradizionale -for-loop (che utilizza l'altra risposta) - non rendendomi conto che prima era il motivo per cui ero motivato a scrivere questa risposta (ho erroneamente pensato allora che fossero tutti i for-loop che avrebbero gettato l'eccezione).
cellepo,

0

Una soluzione potrebbe essere quella di ruotare l'elenco e rimuovere il primo elemento per evitare ConcurrentModificationException o 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

Prova questo (rimuove tutti gli elementi nell'elenco che sono uguali i):

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

0

puoi anche usare la ricorsione

La ricorsione in Java è un processo in cui un metodo si chiama continuamente. Un metodo in Java che si chiama si chiama metodo ricorsivo.


-2

questo potrebbe non essere il modo migliore, ma per la maggior parte dei piccoli casi questo dovrebbe essere accettabile:

"crea un secondo array vuoto e aggiungi solo quelli che vuoi conservare"

Non ricordo da dove ho letto questo ... per giustizia, farò questo wiki nella speranza che qualcuno lo trovi o semplicemente per non guadagnare un rappresentante che non merito.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.