Rimuovi gli elementi dalla raccolta durante l'iterazione


215

AFAIK, ci sono due approcci:

  1. Scorrere su una copia della raccolta
  2. Utilizzare l'iteratore della raccolta effettiva

Per esempio,

List<Foo> fooListCopy = new ArrayList<Foo>(fooList);
for(Foo foo : fooListCopy){
    // modify actual fooList
}

e

Iterator<Foo> itr = fooList.iterator();
while(itr.hasNext()){
    // modify actual fooList using itr.remove()
}

Vi sono ragioni per preferire un approccio rispetto all'altro (ad esempio, preferendo il primo approccio per la semplice ragione della leggibilità)?


1
Solo curioso, perché crei una copia di un folle invece di passare in rassegna il folle nel primo esempio?
Haz,

@Haz, quindi devo fare il loop solo una volta.
user1329572,

15
Nota: preferisci 'for' over 'while' anche con gli iteratori per limitare l'ambito della variabile: for (Iterator <Foo> itr = fooList.iterator (); itr.hasNext ();) {}
Puce

Non sapevo che whileavesse regole di scoping diverse rispetto afor
Alexander Mills,

In una situazione più complessa si potrebbe avere un caso in cui si fooListtrova una variabile di istanza e si chiama un metodo durante il ciclo che finisce per chiamare un altro metodo nella stessa classe che lo fa fooList.remove(obj). L'ho visto succedere. Nel qual caso la copia dell'elenco è più sicura.
Dave Griffiths,

Risposte:


420

Vorrei fare alcuni esempi con alcune alternative per evitare a ConcurrentModificationException.

Supponiamo di avere la seguente raccolta di libri

List<Book> books = new ArrayList<Book>();
books.add(new Book(new ISBN("0-201-63361-2")));
books.add(new Book(new ISBN("0-201-63361-3")));
books.add(new Book(new ISBN("0-201-63361-4")));

Raccogli e rimuovi

La prima tecnica consiste nel raccogliere tutti gli oggetti che vogliamo eliminare (ad es. Usando un ciclo for avanzato) e dopo aver finito l'iterazione, rimuoviamo tutti gli oggetti trovati.

ISBN isbn = new ISBN("0-201-63361-2");
List<Book> found = new ArrayList<Book>();
for(Book book : books){
    if(book.getIsbn().equals(isbn)){
        found.add(book);
    }
}
books.removeAll(found);

Ciò suppone che l'operazione che si desidera eseguire sia "elimina".

Se si desidera "aggiungere" anche questo approccio funzionerebbe, ma suppongo che si ripeterà una raccolta diversa per determinare quali elementi si desidera aggiungere a una seconda raccolta e quindi emettere un addAllmetodo alla fine.

Utilizzando ListIterator

Se si lavora con elenchi, un'altra tecnica consiste nell'utilizzare un ListIteratorsupporto per la rimozione e l'aggiunta di elementi durante l'iterazione stessa.

ListIterator<Book> iter = books.listIterator();
while(iter.hasNext()){
    if(iter.next().getIsbn().equals(isbn)){
        iter.remove();
    }
}

Ancora una volta, ho usato il metodo "rimuovi" nell'esempio sopra, che è quello che la tua domanda sembra implicare, ma puoi anche usare il suo addmetodo per aggiungere nuovi elementi durante l'iterazione.

Utilizzando JDK> = 8

Per coloro che lavorano con Java 8 o versioni superiori, ci sono un paio di altre tecniche che è possibile utilizzare per trarne vantaggio.

È possibile utilizzare il nuovo removeIfmetodo nella Collectionclasse base:

ISBN other = new ISBN("0-201-63361-2");
books.removeIf(b -> b.getIsbn().equals(other));

Oppure usa la nuova API stream:

ISBN other = new ISBN("0-201-63361-2");
List<Book> filtered = books.stream()
                           .filter(b -> b.getIsbn().equals(other))
                           .collect(Collectors.toList());

In quest'ultimo caso, per filtrare gli elementi fuori da una raccolta, è necessario riassegnare il riferimento originale alla raccolta filtrata (ad esempio books = filtered) o utilizzare la raccolta filtrata per removeAllgli elementi trovati della raccolta originale (ad es.books.removeAll(filtered) .).

Usa sublista o sottoinsieme

Ci sono anche altre alternative. Se l'elenco è ordinato e si desidera rimuovere elementi consecutivi, è possibile creare un elenco secondario e quindi cancellarlo:

books.subList(0,5).clear();

Poiché l'elenco secondario è supportato dall'elenco originale, questo sarebbe un modo efficace per rimuovere questa raccolta secondaria di elementi.

Qualcosa di simile potrebbe essere ottenuto con insiemi ordinati usando il NavigableSet.subSetmetodo o uno qualsiasi dei metodi di slicing offerti lì.

considerazioni:

Quale metodo usi potrebbe dipendere da ciò che intendi fare

  • Il colleziona e removeAl tecnica funzionano con qualsiasi raccolta (raccolta, elenco, set, ecc.).
  • La ListIteratortecnica ovviamente funziona solo con gli elenchi, a condizione che la loro ListIteratorimplementazione fornita offra supporto per le operazioni di aggiunta e rimozione.
  • L' Iteratorapproccio funzionerebbe con qualsiasi tipo di raccolta, ma supporta solo le operazioni di rimozione.
  • Con l' approccio ListIterator/ Iteratorl'ovvio vantaggio non è di dover copiare nulla poiché rimuoviamo mentre ripetiamo. Quindi, questo è molto efficiente.
  • L'esempio dei flussi JDK 8 in realtà non ha rimosso nulla, ma ha cercato gli elementi desiderati, quindi abbiamo sostituito il riferimento della raccolta originale con quello nuovo e abbiamo lasciato che il vecchio venisse raccolto. Quindi, ripetiamo solo una volta la raccolta e sarebbe efficace.
  • Nella raccolta e removeAll approccio lo svantaggio è che dobbiamo iterare due volte. Per prima cosa iteriamo nel ciclo di ricerca alla ricerca di un oggetto che soddisfi i nostri criteri di rimozione e, una volta trovato, chiediamo di rimuoverlo dalla raccolta originale, il che implicherebbe un secondo lavoro di iterazione per cercare questo oggetto al fine di rimuoverla.
  • Penso che valga la pena ricordare che il metodo di rimozione Iteratordell'interfaccia è contrassegnato come "opzionale" in Javadocs, il che significa che potrebbero esserci Iteratorimplementazioni che possono essere lanciate UnsupportedOperationExceptionse invochiamo il metodo di rimozione. In quanto tale, direi che questo approccio è meno sicuro di altri se non possiamo garantire il supporto dell'iteratore per la rimozione di elementi.

Bravo! questa è la guida definitiva.
Magno C,

Questa è una risposta perfetta! Grazie.
Wilhelm,

7
Nel tuo paragrafo sui flussi JDK8 di cui parli removeAll(filtered). Una scorciatoia per questo sarebberemoveIf(b -> b.getIsbn().equals(other))
ifloop

Qual è la differenza tra Iterator e ListIterator?
Alexander Mills,

Non ho considerato di rimuoverlo, ma era la risposta alle mie preghiere. Grazie!
Akabelle,

15

In Java 8, c'è un altro approccio. Collection # removeIf

per esempio:

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

list.removeIf(i -> i > 2);

13

Ci sono dei motivi per preferire un approccio rispetto all'altro

Il primo approccio funzionerà, ma ha l'ovvio sovraccarico di copiare l'elenco.

Il secondo approccio non funzionerà perché molti contenitori non consentono modifiche durante l'iterazione. Questo includeArrayList .

Se l'unica modifica è rimuovere l'elemento corrente, è possibile effettuare la seconda opera approccio base itr.remove()(cioè, utilizzare l'iteratore 's remove()metodo, non il contenitore ' s). Questo sarebbe il mio metodo preferito per gli iteratori che supportano remove().


Oops, scusa ... è implicito che avrei usato il metodo di rimozione dell'iteratore, non il contenitore. E quanto sovraccarico crea la copia dell'elenco? Non può essere molto e dato che è mirato a un metodo, dovrebbe essere raccolto in modo piuttosto rapido. Vedi modifica ..
user1329572,

1
@aix Penso che valga la pena ricordare che il metodo di rimozione Iteratordell'interfaccia è contrassegnato come facoltativo in Javadocs, il che significa che potrebbero esserci implementazioni di Iterator che potrebbero essere lanciate UnsupportedOperationException. In quanto tale, direi che questo approccio è meno sicuro del primo. A seconda delle implementazioni che si intende utilizzare, il primo approccio potrebbe essere più adatto.
Edwin Dalorzo,

@EdwinDalorzo remove()della stessa collezione originale può anche lanciare UnsupportedOperationException: docs.oracle.com/javase/7/docs/api/java/util/… . Le interfacce del contenitore Java sono, purtroppo, definite estremamente inaffidabili (sconfiggendo il punto dell'interfaccia, onestamente). Se non conosci l'esatta implementazione che verrà utilizzata in fase di esecuzione, è meglio fare le cose in modo immutabile - ad esempio, utilizzare l'API Java 8+ Streams per filtrare gli elementi e raccoglierli in un nuovo contenitore, quindi sostituisci del tutto quello vecchio con esso.
Matteo Leggi il

5

Funzionerà solo il secondo approccio. È possibile modificare la raccolta durante l'iterazione usando iterator.remove()solo. Tutti gli altri tentativi causeranno ConcurrentModificationException.


2
Il primo tentativo scorre una copia, il che significa che può modificare l'originale.
Colin D,

3

Vecchio timer preferito (funziona ancora):

List<String> list;

for(int i = list.size() - 1; i >= 0; --i) 
{
        if(list.get(i).contains("bad"))
        {
                list.remove(i);
        }
}

Benefici:

  1. Ripete l'elenco una sola volta
  2. Nessun oggetto aggiuntivo creato o altra complessità non necessaria
  3. Nessun problema con il tentativo di utilizzare l'indice di un elemento rimosso, perché ... beh, pensaci!

1

Non puoi fare il secondo, perché anche se usi il remove()metodo su Iterator , otterrai un'eccezione .

Personalmente, preferirei il primo per tutte le Collectionistanze, nonostante l'ulteriore ascolto della creazione del nuovo Collection, lo trovo meno soggetto a errori durante la modifica da parte di altri sviluppatori. In alcune implementazioni di Collection, Iterator remove()è supportato, in altri no. Puoi leggere di più nei documenti per Iterator .

La terza alternativa, è creare un nuovo Collection, scorrere sull'originale e aggiungere tutti i membri del primo Collectional secondo Collectionche non sono disponibili per l'eliminazione. A seconda delle dimensioni Collectione del numero di eliminazioni, ciò potrebbe consentire un notevole risparmio di memoria rispetto al primo approccio.


0

Sceglierei il secondo in quanto non è necessario eseguire una copia della memoria e Iterator funziona più velocemente. Quindi risparmi memoria e tempo.


" Iterator funziona più velocemente ". Qualcosa a supporto di questa affermazione? Inoltre, l'impronta di memoria della creazione di una copia di un elenco è molto banale, soprattutto perché sarà nell'ambito di un metodo e sarà immondizia raccolta quasi immediatamente.
user1329572,

1
Nel primo approccio lo svantaggio è che dobbiamo ripetere due volte. Esaminiamo il ciclo di ricerca alla ricerca di un elemento e, una volta trovato, chiediamo di rimuoverlo dall'elenco originale, il che implicherebbe un secondo lavoro di iterazione per cercare questo dato elemento. Ciò sosterrebbe l'affermazione secondo cui, almeno in questo caso, l'approccio iteratore dovrebbe essere più rapido. Dobbiamo considerare che solo lo spazio strutturale della raccolta è quello che viene creato, gli oggetti all'interno delle raccolte non vengono copiati. Entrambe le raccolte manterrebbero riferimenti agli stessi oggetti. Quando succede GC non possiamo dirlo !!!
Edwin Dalorzo,

-2

perché non questo?

for( int i = 0; i < Foo.size(); i++ )
{
   if( Foo.get(i).equals( some test ) )
   {
      Foo.remove(i);
   }
}

E se è una mappa, non un elenco, puoi usare keyset ()


4
Questo approccio presenta molti importanti svantaggi. Innanzitutto, ogni volta che rimuovi un elemento, gli indici vengono riorganizzati. Pertanto, se si rimuove l'elemento 0, l'elemento 1 diventa il nuovo elemento 0. Se si accede a questo, almeno farlo all'indietro per evitare questo problema. In secondo luogo, non tutte le implementazioni di List offrono accesso diretto agli elementi (come ArrayList). In una LinkedList questo sarebbe tremendamente inefficiente perché ogni volta che lo emetti get(i)devi visitare tutti i nodi fino a raggiungere i.
Edwin Dalorzo,

Non l'ho mai considerato perché in genere l'ho usato solo per rimuovere un singolo oggetto che stavo cercando. Buono a sapersi.
Drake Clarris,

4
Sono in ritardo alla festa, ma sicuramente nel codice di blocco if dopo Foo.remove(i);che dovresti farlo i--;?
Bertie Wheen,

perché è infastidito
Jack
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.