Rimuovi elementi dalla raccolta mentre si itera

AFAIK, ci sono due approcci:

  1. Passare sopra una copia della collezione
  2. Usa l’iteratore della collezione attuale

Per esempio,

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

e

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

Ci sono motivi per preferire un approccio all’altro (ad esempio preferendo il primo approccio per la semplice ragione della leggibilità)?

Lasciatemi fare alcuni esempi con alcune alternative per evitare una ConcurrentModificationException .

Supponiamo di avere la seguente collezione di libri

 List books = new ArrayList(); 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 (es. Usando un ciclo for perfezionato) e dopo aver terminato l’iterazione, rimuoviamo tutti gli oggetti trovati.

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

Questo è supposto che l’operazione che vuoi fare sia “cancellare”.

Se si desidera “aggiungere”, questo approccio potrebbe anche funzionare, ma suppongo che si eseguirà un’iterazione su una raccolta diversa per determinare quali elementi si desidera aggiungere a una seconda raccolta e quindi emettere un metodo addAll alla fine.

Utilizzando ListIterator

Se si lavora con elenchi, un’altra tecnica consiste nell’utilizzare un ListIterator che supporta la rimozione e l’aggiunta di elementi durante l’iterazione stessa.

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

Ancora una volta, ho usato il metodo “remove” nell’esempio sopra che è ciò che la tua domanda sembrava implicare, ma puoi anche usare il suo metodo add per aggiungere nuovi elementi durante l’iterazione.

Utilizzando JDK 8

Per chi lavora con Java 8 o versioni superiori, ci sono un paio di altre tecniche che è ansible utilizzare per trarne vantaggio.

Puoi utilizzare il nuovo metodo removeIf nella class base Collection :

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

Oppure utilizza la nuova API di streaming:

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

In quest’ultimo caso, per filtrare gli elementi da una raccolta, riassegni il riferimento originale alla raccolta filtrata (cioè books = filtered ) o utilizzi la raccolta filtrata per removeAll gli elementi trovati dalla raccolta originale (ad es. books.removeAll(filtered) ).

Usa sottoelenco o sottoinsieme

Ci sono anche altre alternative. Se l’elenco è ordinato e si desidera rimuovere elementi consecutivi, è ansible creare un sottolista e quindi cancellarlo:

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

Poiché la sottolista è supportata dall’elenco originale, questo sarebbe un modo efficace per rimuovere questa sottoraccolta di elementi.

Qualcosa di simile potrebbe essere ottenuto con set ordinati usando il metodo NavigableSet.subSet o uno dei metodi di slicing offerti lì.

considerazioni:

Il metodo che utilizzi potrebbe dipendere da ciò che intendi fare

  • La tecnica collect e removeAl funziona con qualsiasi Collection (Collection, List, Set, ecc.).
  • La tecnica ListIterator ovviamente solo con le liste, a condizione che la loro implementazione ListIterator fornita offra supporto per operazioni di aggiunta e rimozione.
  • L’approccio di Iterator funziona con qualsiasi tipo di raccolta, ma supporta solo le operazioni di rimozione.
  • Con l’approccio ListIterator / Iterator , l’ovvio vantaggio non è la necessità di copiare nulla dal momento che rimuoviamo mentre iteriamo. Quindi, questo è molto efficiente.
  • L’esempio dei flussi JDK 8 in realtà non rimuove nulla, ma cerca gli elementi desiderati, quindi sostituiamo il riferimento di raccolta originale con quello nuovo e lasciamo che quello vecchio venga raccolto. Quindi, iteriamo solo una volta sopra la raccolta e ciò sarebbe efficiente.
  • Nell’approccio collect and removeAll lo svantaggio è che dobbiamo ripetere l’iterazione due volte. Per prima cosa eseguiamo l’iterazione nel ciclo di lavoro alla ricerca di un object che corrisponda ai 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 elemento al fine di rimuoverla.
  • Penso che valga la pena ricordare che il metodo di rimozione dell’interfaccia Iterator è contrassegnato come “facoltativo” in Javadocs, il che significa che potrebbero esserci implementazioni di Iterator che generano UnsupportedOperationException se invochiamo il metodo remove. Pertanto, direi che questo approccio è meno sicuro di altri se non possiamo garantire il supporto iteratore per la rimozione di elementi.

Ci sono motivi per preferire un approccio all’altro

Il primo approccio funzionerà, ma ha l’ovvio sovraccarico di copiare la lista.

Il secondo approccio non funzionerà perché molti contenitori non consentono la modifica durante l’iterazione. Questo include ArrayList .

Se la sola modifica è quella di rimuovere l’elemento corrente, puoi fare in modo che il secondo approccio funzioni itr.remove() (cioè, usa il metodo remove() itr.remove() , non quello del contenitore ). Questo sarebbe il mio metodo preferito per gli iteratori che supportano remove() .

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

per esempio:

 List list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); list.removeIf(i -> i > 2); 

Solo il secondo approccio funzionerà. È ansible modificare la raccolta durante l’iterazione utilizzando solo iterator.remove() . Tutti gli altri tentativi causeranno ConcurrentModificationException .

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

Personalmente, preferirei la prima per tutte le istanze di Collection , nonostante l’ulteriore difficoltà di creare la nuova Collection , lo trovo meno sobject a errori durante la modifica da parte di altri sviluppatori. Su alcune implementazioni della Collezione, l’Iterator remove() è supportato, in altre non lo è. Puoi leggere di più nella documentazione di Iterator .

La terza alternativa consiste nel creare una nuova Collection , scorrere l’originale e aggiungere tutti i membri della prima Collection alla seconda Collection che non sono stati cancellati. A seconda delle dimensioni della Collection e del numero di eliminazioni, ciò potrebbe far risparmiare in modo significativo sulla memoria, rispetto al primo approccio.

Sceglierei il secondo in quanto non devi fare una copia della memoria e l’Iterator funziona più velocemente. Quindi risparmi la memoria e il tempo.

perché non questo?

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

E se si tratta di una mappa, non di una lista, puoi usare keyset ()

C’è anche una soluzione semplice per “iterare” una Collection e rimuovere ogni elemento.

 List list = new ArrayList<>(); //Fill the list 

È semplicemente concisto sul loop fino a quando l’elenco non è vuoto e su ogni iterazione rimuoviamo il primo elemento con remove(0) .

 while(!list.isEmpty()){ String s = list.remove(0); // do you thing } 

Non credo che questo abbia alcun miglioramento rispetto a Iterator , è comunque necessario avere una lista mutabile ma mi piace la semplicità di questa soluzione.