Entfernen Sie während der Iteration Elemente aus der Sammlung

215

AFAIK, es gibt zwei Ansätze:

  1. Durchlaufen Sie eine Kopie der Sammlung
  2. Verwenden Sie den Iterator der tatsächlichen Sammlung

Zum Beispiel,

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

und

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

Gibt es Gründe, einen Ansatz dem anderen vorzuziehen (z. B. den ersten Ansatz aus einfachen Gründen der Lesbarkeit zu bevorzugen)?

user1329572
quelle
1
Nur neugierig, warum erstellen Sie eine Kopie des Narren, anstatt im ersten Beispiel nur den Narren zu durchlaufen?
Haz
@Haz, also muss ich nur einmal schleifen.
user1329572
15
Hinweis: Bevorzugen Sie 'für' gegenüber 'während' auch mit Iteratoren, um den Umfang der Variablen einzuschränken: für (Iterator <Foo> itr = fooList.iterator (); itr.hasNext ();) {}
Puce
Ich wusste nicht, whiledass andere Regeln gelten alsfor
Alexander Mills
In einer komplexeren Situation kann es vorkommen, dass es sich fooListum eine Instanzvariable handelt und Sie während der Schleife eine Methode aufrufen, die schließlich eine andere Methode in derselben Klasse aufruft fooList.remove(obj). Habe das gesehen. In diesem Fall ist das Kopieren der Liste am sichersten.
Dave Griffiths

Antworten:

415

Lassen Sie mich einige Beispiele mit Alternativen nennen, um a zu vermeiden ConcurrentModificationException.

Angenommen, wir haben die folgende Büchersammlung

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

Sammeln und entfernen

Die erste Technik besteht darin, alle Objekte zu sammeln, die wir löschen möchten (z. B. mithilfe einer erweiterten for-Schleife), und nach Abschluss der Iteration alle gefundenen Objekte zu entfernen.

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

Dies setzt voraus, dass der Vorgang, den Sie ausführen möchten, "Löschen" ist.

Wenn Sie diesen Ansatz "hinzufügen" möchten, funktioniert er auch. Ich gehe jedoch davon aus, dass Sie eine andere Sammlung durchlaufen, um zu bestimmen, welche Elemente Sie einer zweiten Sammlung hinzufügen möchten, und addAllam Ende eine Methode ausgeben .

ListIterator verwenden

Wenn Sie mit Listen arbeiten, besteht eine andere Technik darin, eine zu verwenden, ListIteratordie das Entfernen und Hinzufügen von Elementen während der Iteration selbst unterstützt.

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

Auch hier habe ich im obigen Beispiel die Methode "Entfernen" verwendet, was Ihre Frage zu implizieren schien. Sie können jedoch auch die addMethode verwenden, um während der Iteration neue Elemente hinzuzufügen.

Verwenden von JDK> = 8

Für diejenigen, die mit Java 8 oder höheren Versionen arbeiten, gibt es einige andere Techniken, die Sie verwenden können, um davon zu profitieren.

Sie können die neue removeIfMethode in der CollectionBasisklasse verwenden:

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

Oder verwenden Sie die neue Stream-API:

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

In diesem letzten Fall weisen Sie zum Filtern von Elementen aus einer Sammlung den ursprünglichen Verweis der gefilterten Sammlung (dh books = filtered) neu zu oder verwenden die gefilterte Sammlung removeAllden gefundenen Elementen aus der ursprünglichen Sammlung (dh books.removeAll(filtered)).

Verwenden Sie Unterliste oder Teilmenge

Es gibt auch andere Alternativen. Wenn die Liste sortiert ist und Sie aufeinanderfolgende Elemente entfernen möchten, können Sie eine Unterliste erstellen und diese dann löschen:

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

Da die Unterliste durch die ursprüngliche Liste unterstützt wird, wäre dies eine effiziente Möglichkeit, diese Unterkollektion von Elementen zu entfernen.

Ähnliches könnte mit sortierten Sätzen unter Verwendung der NavigableSet.subSetMethode oder einer der dort angebotenen Schneidemethoden erreicht werden.

Überlegungen:

Welche Methode Sie verwenden, hängt möglicherweise davon ab, was Sie vorhaben

  • Das Sammeln und die removeAlTechnik funktionieren mit jeder Sammlung (Sammlung, Liste, Satz usw.).
  • Die ListIteratorTechnik funktioniert offensichtlich nur mit Listen, vorausgesetzt, die angegebene ListIteratorImplementierung bietet Unterstützung für das Hinzufügen und Entfernen von Vorgängen.
  • Der IteratorAnsatz funktioniert mit jeder Art von Sammlung, unterstützt jedoch nur Entfernungsvorgänge.
  • Mit dem ListIterator/ Iterator-Ansatz besteht der offensichtliche Vorteil darin, dass nichts kopiert werden muss, da wir es beim Iterieren entfernen. Das ist also sehr effizient.
  • Das Beispiel für JDK 8-Streams hat eigentlich nichts entfernt, sondern nach den gewünschten Elementen gesucht. Dann haben wir die ursprüngliche Sammlungsreferenz durch die neue ersetzt und die alte als Müllsammlung verwendet. Wir iterieren also nur einmal über die Sammlung, und das wäre effizient.
  • Beim Sammeln und removeAllAnnähern besteht der Nachteil darin, dass wir zweimal iterieren müssen. Zuerst iterieren wir in der Foor-Schleife nach einem Objekt, das unseren Entfernungskriterien entspricht, und sobald wir es gefunden haben, bitten wir Sie, es aus der ursprünglichen Sammlung zu entfernen, was eine zweite Iterationsarbeit bedeuten würde, um nach diesem Element zu suchen entfernen Sie es.
  • Ich denke, es ist erwähnenswert, dass die Methode remove der IteratorSchnittstelle in Javadocs als "optional" markiert ist, was bedeutet, dass es IteratorImplementierungen geben kann, die ausgelöst werden, UnsupportedOperationExceptionwenn wir die Methode remove aufrufen. Daher würde ich sagen, dass dieser Ansatz weniger sicher ist als andere, wenn wir die Iterator-Unterstützung für das Entfernen von Elementen nicht garantieren können.
Edwin Dalorzo
quelle
Bravo! Dies ist der endgültige Leitfaden.
Magno C
Dies ist eine perfekte Antwort! Danke dir.
Wilhelm
6
In Ihrem Absatz über JDK8-Streams erwähnen Sie removeAll(filtered). Eine Abkürzung dafür wäreremoveIf(b -> b.getIsbn().equals(other))
ifloop
Was ist der Unterschied zwischen Iterator und ListIterator?
Alexander Mills
Ich habe nicht daran gedacht, es zu entfernen, aber es war die Antwort auf meine Gebete. Vielen Dank!
Akabelle
15

In Java 8 gibt es einen anderen Ansatz. Sammlung # removeIf

z.B:

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

list.removeIf(i -> i > 2);
Santhosh
quelle
13

Gibt es Gründe, einen Ansatz dem anderen vorzuziehen?

Der erste Ansatz funktioniert, hat jedoch den offensichtlichen Aufwand, die Liste zu kopieren.

Der zweite Ansatz funktioniert nicht, da viele Container keine Änderungen während der Iteration zulassen. Dies beinhaltetArrayList .

Wenn die einzige Änderung das aktuelle Element zu entfernen, können Sie den zweiten Ansatz funktioniert durch die Verwendung itr.remove()(das heißt, verwenden Sie den Iterator ‚s remove()Methode, nicht die Container ‘ s). Dies wäre meine bevorzugte Methode für Iteratoren, die dies unterstützen remove().

NPE
quelle
Ups, sorry ... es wird vorausgesetzt, dass ich die Entfernungsmethode des Iterators verwenden würde, nicht die des Containers. Und wie viel Aufwand verursacht das Kopieren der Liste? Es kann nicht viel sein und da es sich um eine Methode handelt, sollte der Müll ziemlich schnell gesammelt werden. Siehe bearbeiten ..
user1329572
1
@aix Ich denke, es ist erwähnenswert, dass die Methode zum Entfernen der IteratorSchnittstelle in Javadocs als optional markiert ist, was bedeutet, dass es Iterator-Implementierungen geben kann, die möglicherweise ausgelöst werden UnsupportedOperationException. Daher würde ich sagen, dass dieser Ansatz weniger sicher ist als der erste. Abhängig von den Implementierungen, die verwendet werden sollen, könnte der erste Ansatz besser geeignet sein.
Edwin Dalorzo
@EdwinDalorzo remove()auf der Originalsammlung selbst kann auch werfen UnsupportedOperationException: docs.oracle.com/javase/7/docs/api/java/util/… . Die Java-Container-Schnittstellen sind leider als äußerst unzuverlässig definiert (ehrlich gesagt, wenn man den Punkt der Schnittstelle besiegt). Wenn Sie nicht genau wissen, welche Implementierung zur Laufzeit verwendet wird, ist es besser, die Dinge unveränderlich zu machen. Verwenden Sie beispielsweise die Java 8+ Streams-API, um die Elemente nach unten zu filtern und in einem neuen Container zu sammeln Ersetzen Sie das alte vollständig damit.
Matthew Read
5

Nur der zweite Ansatz wird funktionieren. Sie können die Sammlung nur während der Iteration ändern iterator.remove(). Alle anderen Versuche werden verursachen ConcurrentModificationException.

AlexR
quelle
2
Der erste Versuch wiederholt sich auf einer Kopie, was bedeutet, dass er das Original ändern kann.
Colin D
2

Oldtimer Favorit (es funktioniert immer noch):

List<String> list;

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

Sie können die zweite nicht ausführen, da selbst wenn Sie die remove()Methode für Iterator verwenden , eine Ausnahme ausgelöst wird .

Persönlich würde ich die erste für alle CollectionInstanzen bevorzugen , obwohl Collectionich zusätzlich gehört habe, wie die neue erstellt wird , finde ich sie weniger fehleranfällig bei der Bearbeitung durch andere Entwickler. Bei einigen Collection-Implementierungen wird der Iterator remove()unterstützt, bei anderen nicht. Weitere Informationen finden Sie in den Dokumenten zu Iterator .

Die dritte Alternative besteht darin, ein neues zu erstellen Collection, das Original zu durchlaufen und alle Mitglieder des ersten Collectionzum zweiten hinzuzufügen Collection, die nicht gelöscht werden können. Abhängig von der Größe der Collectionund der Anzahl der Löschvorgänge kann dies im Vergleich zum ersten Ansatz erheblich Speicherplatz sparen.

Jon
quelle
0

Ich würde die zweite wählen, da Sie keine Kopie des Speichers erstellen müssen und der Iterator schneller arbeitet. So sparen Sie Speicher und Zeit.

Calin Andrei
quelle
" Iterator arbeitet schneller ". Gibt es etwas, das diese Behauptung stützt? Außerdem ist der Speicherbedarf beim Erstellen einer Kopie einer Liste sehr gering, insbesondere da dieser innerhalb einer Methode festgelegt wird und der Müll fast sofort gesammelt wird.
user1329572
1
Beim ersten Ansatz besteht der Nachteil darin, dass wir zweimal iterieren müssen. Wir iterieren in der Foor-Schleife und suchen nach einem Element. Sobald wir es gefunden haben, bitten wir Sie, es aus der ursprünglichen Liste zu entfernen. Dies würde eine zweite Iterationsarbeit bedeuten, um nach diesem bestimmten Element zu suchen. Dies würde die Behauptung stützen, dass der Iteratoransatz zumindest in diesem Fall schneller sein sollte. Wir müssen berücksichtigen, dass nur der strukturelle Raum der Sammlung erstellt wird und die Objekte in den Sammlungen nicht kopiert werden. Beide Sammlungen würden Verweise auf dieselben Objekte behalten. Wann GC passiert, können wir nicht sagen !!!
Edwin Dalorzo
-2

warum nicht das?

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

Und wenn es sich um eine Karte handelt, nicht um eine Liste, können Sie Keyset () verwenden.

Drake Clarris
quelle
4
Dieser Ansatz hat viele Hauptnachteile. Zunächst werden die Indizes jedes Mal neu organisiert, wenn Sie ein Element entfernen. Wenn Sie also Element 0 entfernen, wird das Element 1 zum neuen Element 0. Wenn Sie dazu gehen, machen Sie es zumindest rückwärts, um dieses Problem zu vermeiden. Zweitens bieten nicht alle List-Implementierungen direkten Zugriff auf die Elemente (wie dies bei ArrayList der Fall ist). In einer LinkedList wäre dies enorm ineffizient, da Sie jedes Mal, wenn Sie eine ausgeben get(i), alle Knoten besuchen müssen, bis Sie sie erreichen i.
Edwin Dalorzo
Ich habe dies nie in Betracht gezogen, da ich es normalerweise nur verwendet habe, um einen einzelnen Gegenstand zu entfernen, den ich gesucht habe. Gut zu wissen.
Drake Clarris
4
Ich bin zu spät zur Party, aber sicher im if-Blockcode, nachdem Foo.remove(i);du es tun sollst i--;?
Bertie Wheen
weil es abgehört ist
Jack