Entfernen Sie alle Vorkommen eines bestimmten Werts aus einer Liste

Entfernen Sie alle Vorkommen eines bestimmten Werts aus einer Liste

1. Einführung

In Java ist es einfach, einen bestimmten Wert mitList.remove() ausList zu entfernen. efficiently removing all occurrences of a value ist jedoch viel schwieriger.

In diesem Tutorial sehen wir mehrere Lösungen für dieses Problem, in denen die Vor- und Nachteile beschrieben werden.

Aus Gründen der Lesbarkeit verwenden wir in den Tests eine benutzerdefiniertelist(int…)-Methode, dieArrayList mit den übergebenen Elementen zurückgibt.

2. Verwenden einerwhile-Schleife

Da wir wissen, wieremove a single element, doing it repeatedly in a loop einfach genug aussieht:

void removeAll(List list, int element) {
    while (list.contains(element)) {
        list.remove(element);
    }
}

Es funktioniert jedoch nicht wie erwartet:

// given
List list = list(1, 2, 3);
int valueToRemove = 1;

// when
assertThatThrownBy(() -> removeAll(list, valueToRemove))
  .isInstanceOf(IndexOutOfBoundsException.class);

Das Problem ist in der 3. Zeile: Wir nennenList.remove(int), which treats its argument as the index, not the value we want to remove.

Im obigen Test rufen wir immerlist.remove(1) auf, aber der Index des Elements, das wir entfernen möchten, ist0.. Wenn SieList.remove() aufrufen, werden alle Elemente nach dem entfernten auf kleinere Indizes verschoben.

In diesem Szenario bedeutet dies, dass wir alle Elemente mit Ausnahme des ersten löschen.

Wenn nur der erste übrig bleibt, ist der Index1 unzulässig. Daher erhalten wir einException.

Beachten Sie, dass wir diesem Problem nur begegnen, wenn wirList.remove() mit einem primitiven Argumentbyte,short, char oderint aufrufen, da der Compiler als erstes versucht, es zu finden Die passende überladene Methode erweitert sich.

Wir können es korrigieren, indem wir den Wert alsInteger: übergeben

void removeAll(List list, Integer element) {
    while (list.contains(element)) {
        list.remove(element);
    }
}

Jetzt funktioniert der Code wie erwartet:

// given
List list = list(1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

DaList.contains() undList.remove() beide das erste Auftreten des Elements finden müssen, verursacht dieser Code eine unnötige Elementdurchquerung.

Wir können es besser machen, wenn wir den Index des ersten Auftretens speichern:

void removeAll(List list, Integer element) {
    int index;
    while ((index = list.indexOf(element)) >= 0) {
        list.remove(index);
    }
}

Wir können überprüfen, ob es funktioniert:

// given
List list = list(1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

Während diese Lösungen kurzen und sauberen Code erzeugen,they still have poor performance: Da wir den Fortschritt nicht verfolgen, mussList.remove() das erste Auftreten des angegebenen Werts finden, um ihn zu löschen.

Wenn wirArrayList verwenden, kann die Elementverschiebung viele Referenzkopien verursachen und sogar das Backing-Array mehrmals neu zuweisen.

3. Entfernen, bis sichList ändert

List.remove(E element) hat eine Funktion, die wir noch nicht erwähnt haben:returns a boolean value, which is true if the List changed because of the operation, therefore it contained the element.

Beachten Sie, dassList.remove(int index) void zurückgibt, denn wenn der angegebene Index gültig ist, wird er vonList immer entfernt. Andernfalls wirdIndexOutOfBoundsException ausgelöst.

Damit können wir Entfernungen durchführen, bis sichList ändert:

void removeAll(List list, int element) {
    while (list.remove(element));
}

Es funktioniert wie erwartet:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

Obwohl diese Implementierung kurz ist, weist sie dieselben Probleme auf, die wir im vorherigen Abschnitt beschrieben haben.

3. Verwenden einerfor-Schleife

Wir können unseren Fortschritt verfolgen, indem wir die Elemente mit einerfor-Schleife durchlaufen und die aktuelle entfernen, wenn sie übereinstimmt:

void removeAll(List list, int element) {
    for (int i = 0; i < list.size(); i++) {
        if (Objects.equals(element, list.get(i))) {
            list.remove(i);
        }
    }
}

Es funktioniert wie erwartet:

// given
List list = list(1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

Wenn wir es jedoch mit einer anderen Eingabe versuchen, liefert es eine falsche Ausgabe:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(1, 2, 3));

Lassen Sie uns Schritt für Schritt analysieren, wie der Code funktioniert:

  • i = 0

    • element undlist.get(i) sind beide gleich1 in Zeile 3, sodass Java in den Hauptteil der Anweisungif eingeht.

    • wir entfernen das Element bei Index0,

    • list enthält jetzt1,2 und3

  • i = 1

    • list.get(i) gibt2 zurück, weilwhen we remove an element from a List, it shifts all proceeding elements to smaller indices

Wir stehen also vor diesem Problem, wennwe have two adjacent values, which we want to remove. Um dies zu lösen, sollten wir die Schleifenvariable beibehalten.

Verringern Sie es, wenn wir das Element entfernen:

void removeAll(List list, int element) {
    for (int i = 0; i < list.size(); i++) {
        if (Objects.equals(element, list.get(i))) {
            list.remove(i);
            i--;
        }
    }
}

Erhöhen Sie es nur, wenn wir das Element nicht entfernen:

void removeAll(List list, int element) {
    for (int i = 0; i < list.size();) {
        if (Objects.equals(element, list.get(i))) {
            list.remove(i);
        } else {
            i++;
        }
    }
}

Beachten Sie, dass wir in letzterem die Anweisungi++ in Zeile 2 entfernt haben.

Beide Lösungen funktionieren wie erwartet:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

Diese Implementierung erscheint auf den ersten Blick richtig. Es hat jedoch immer nochserious performance problems:

  • Wenn Sie ein Element aus einemArrayList entfernen, werden alle Elemente danach verschoben

  • Der Zugriff auf Elemente nach Index inLinkedList bedeutet, die Elemente einzeln zu durchlaufen, bis der Index gefunden ist

4. Verwenden einerfor-each-Schleife

Seit Java 5 können wir diefor-each-Schleife verwenden, um durchList zu iterieren. Verwenden wir es, um Elemente zu entfernen:

void removeAll(List list, int element) {
    for (Integer number : list) {
        if (Objects.equals(number, element)) {
            list.remove(number);
        }
    }
}

Beachten Sie, dass wirInteger als Typ der Schleifenvariablen verwenden. Daher erhalten wir keineNullPointerException.

Auf diese Weise rufen wir auchList.remove(E element) auf, das den Wert erwartet, den wir entfernen möchten, nicht den Index.

So sauber es auch aussieht, leider funktioniert es nicht:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
assertThatThrownBy(() -> removeWithForEachLoop(list, valueToRemove))
  .isInstanceOf(ConcurrentModificationException.class);

Diefor-each-Schleife verwendetIterator, um die Elemente zu durchlaufen. when we modify the List, the Iterator gets into an inconsistent state. Hence it throws ConcurrentModificationException.

Die Lektion lautet: Wir sollten einList nicht ändern, während wir in einerfor-each-Schleife auf seine Elemente zugreifen.

5. Mit einemIterator

Wir können dieIterator direkt verwenden, um dieList damit zu durchlaufen und zu modifizieren:

void removeAll(List list, int element) {
    for (Iterator i = list.iterator(); i.hasNext();) {
        Integer number = i.next();
        if (Objects.equals(number, element)) {
            i.remove();
        }
    }
}

Auf diese Weise wirdthe Iterator can track the state of the List (weil die Änderung vorgenommen wird). Daher funktioniert der obige Code wie erwartet:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

Da jedeList-Klasse ihre eigeneIterator-Implementierung bereitstellen kann, können wir davon ausgehen, dassit implements element traversing and removal the most efficient way possible.

However, using ArrayList still means lots of element shifting (und möglicherweise Array-Neuzuweisung). Außerdem ist der obige Code etwas schwieriger zu lesen, da er sich von der Standardschleifeforunterscheidet, mit der die meisten Entwickler vertraut sind.

6. Sammeln

Bis dahin haben wir das ursprünglicheList-Objekt geändert, indem wir die nicht benötigten Elemente entfernt haben. Vielmehr können wircreate a new List and collect the items we want to keep:

List removeAll(List list, int element) {
    List remainingElements = new ArrayList<>();
    for (Integer number : list) {
        if (!Objects.equals(number, element)) {
            remainingElements.add(number);
        }
    }
    return remainingElements;
}

Da wir das Ergebnis in einem neuenList-Objekt bereitstellen, müssen wir es von der Methode zurückgeben. Daher müssen wir die Methode auf eine andere Weise verwenden:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
List result = removeAll(list, valueToRemove);

// then
assertThat(result).isEqualTo(list(2, 3));

Beachten Sie, dass wir jetzt diefor-each-Schleife verwenden können, da wir dieList, die wir gerade durchlaufen, nicht ändern.

Da keine Umzüge vorgenommen werden, müssen die Elemente nicht verschoben werden. Daher funktioniert diese Implementierung gut, wenn wirArrayList. verwenden

Diese Implementierung verhält sich in einigen Punkten anders als die vorherigen:

  • Das ursprünglicheListwird nicht geändert, sondernreturns a new

  • the method decides what the returned List‘s implementation is kann es vom Original abweichen

Außerdem können wir unsere Implementierung aufget the old behavior ändern. Wir löschen die ursprünglichenList und fügen die gesammelten Elemente hinzu:

void removeAll(List list, int element) {
    List remainingElements = new ArrayList<>();
    for (Integer number : list) {
        if (!Objects.equals(number, element)) {
            remainingElements.add(number);
        }
    }

    list.clear();
    list.addAll(remainingElements);
}

Es funktioniert genauso wie zuvor:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

Da wir dieListnicht kontinuierlich ändern, müssen wir nicht auf Elemente nach Position zugreifen oder sie verschieben. Außerdem gibt es nur zwei mögliche Neuzuordnungen von Arrays: Wenn wirList.clear() undList.addAll() aufrufen.

7. Verwenden der Stream-API

In Java 8 wurden Lambda-Ausdrücke und die Stream-API eingeführt. Mit diesen leistungsstarken Funktionen können wir unser Problem mit einem sehr sauberen Code lösen:

List removeAll(List list, int element) {
    return list.stream()
      .filter(e -> !Objects.equals(e, element))
      .collect(Collectors.toList());
}

Diese Lösungworks the same way, like when we were collecting the remaining elements.

As a result, it has the same characteristics, und wir sollten es verwenden, um das Ergebnis zurückzugeben:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
List result = removeAll(list, valueToRemove);

// then
assertThat(result).isEqualTo(list(2, 3));

Beachten Sie, dass wir es so konvertieren können, dass es wie die anderen Lösungen funktioniert, und zwar mit dem gleichen Ansatz wie bei der ursprünglichen Implementierung des Sammelns.

8. Verwenden vonremoveIf

Mit Lambdas und funktionalen Schnittstellen führte Java 8 auch einige API-Erweiterungen ein. Zum Beispiel dieList.removeIf() method, which implements what we saw in the last section.

Es wird einPredicate erwartet, dastrue when we want to remove des Elements zurückgeben sollte, im Gegensatz zum vorherigen Beispiel, in dem wirtrue zurückgeben mussten, wenn wir das Element behalten wollten:

void removeAll(List list, int element) {
    list.removeIf(n -> Objects.equals(n, element));
}

Es funktioniert wie die anderen Lösungen oben:

// given
List list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

Aufgrund der Tatsache, dassList selbst diese Methode implementiert, können wir davon ausgehen, dass sie die beste verfügbare Leistung aufweist. Darüber hinaus bietet diese Lösung den saubersten Code von allen.

9. Fazit

In diesem Artikel haben wir viele Möglichkeiten zur Lösung eines einfachen Problems aufgezeigt, einschließlich falscher. Wir haben sie analysiert, um für jedes Szenario die beste Lösung zu finden.

Wie üblich sind die Beispiele inover on GitHub verfügbar.