removeIf Implementierungsdetail

9

Ich habe eine kleine Implementierungsdetailfrage, die ich nicht verstehe ArrayList::removeIf. Ich glaube nicht, dass ich es einfach so ausdrücken kann, wie es ist, ohne vorher einige Voraussetzungen zu haben.

Als solches: Die Implementierung ist im Gegensatz zu einer Masse . Ein Beispiel soll das Verständnis erleichtern. Angenommen, ich habe diese Liste:removeArrayList::remove

List<Integer> list = new ArrayList<>(); // 2, 4, 6, 5, 5
list.add(2);
list.add(4);
list.add(6);
list.add(5);
list.add(5); 

Und ich möchte jedes Element entfernen, das gerade ist. Ich könnte:

Iterator<Integer> iter = list.iterator();
while (iter.hasNext()) {
    int elem = iter.next();
    if (elem % 2 == 0) {
         iter.remove();
    }
}

Oder :

list.removeIf(x -> x % 2 == 0);

Das Ergebnis wird das gleiche sein, aber die Implementierung ist sehr unterschiedlich. Da dies iteratoreine Ansicht von ist ArrayList, muss jedes Mal, wenn ich aufrufe remove, der Basiswert ArrayListin einen "guten" Zustand gebracht werden, was bedeutet, dass sich das innere Array tatsächlich ändert. Auch bei jedem einzelnen Anruf von removewerden System::arrayCopyinterne Anrufe getätigt.

Der Kontrast removeIfist schlauer. Da die Iteration intern durchgeführt wird, können die Dinge optimiert werden. Die Art und Weise, wie dies geschieht, ist interessant.

Zunächst werden die Indizes berechnet, aus denen die Elemente entfernt werden sollen. Dies erfolgt, indem zuerst ein winziges BitSetArray von longWerten berechnet wird, in dem sich an jedem Index ein 64 bitWert (a long) befindet. Mehrere 64 bitWerte machen dies a BitSet. Um einen Wert auf einen bestimmten Versatz festzulegen, müssen Sie zuerst den Index im Array ermitteln und dann das entsprechende Bit setzen. Das ist nicht sehr kompliziert. Angenommen, Sie möchten Bit 65 und 3 setzen. Zuerst benötigen wir ein long [] l = new long[2](weil wir über 64 Bit hinausgegangen sind, aber nicht mehr als 128):

|0...(60 more bits here)...000|0...(60 more bits here)...000|

Sie finden zuerst den Index: 65 / 64(sie tun es tatsächlich 65 >> 6) und geben dann in diesen Index ( 1) das benötigte Bit ein:

1L << 65 // this will "jump" the first 64 bits, so this will actually become 00000...10. 

Gleiches für 3. Als solches wird dieses lange Array:

|0...(60 more bits here)...010|0...(60 more bits here)...1000|

Im Quellcode nennen sie dieses BitSet - deathRow(schöner Name!).


Nehmen wir das evenBeispiel hier, wolist = 2, 4, 6, 5, 5

  • Sie iterieren das Array und berechnen dies deathRow(wo Predicate::testist true).

DeathRow = 7 (000 ... 111)

Bedeutungsindizes = [0, 1, 2] sind zu entfernen

  • Sie ersetzen jetzt Elemente im zugrunde liegenden Array basierend auf dieser DeathRow (ohne auf die Details einzugehen, wie dies gemacht wird).

Das innere Array wird zu: [5, 5, 6, 5, 5]. Grundsätzlich verschieben sie die Elemente, die vor dem Array bleiben sollen.


Ich kann endlich die Frage stellen.

Zu diesem Zeitpunkt wissen sie:

 w   ->  number of elements that have to remain in the list (2)
 es  ->  the array itself ([5, 5, 6, 5, 5])
 end ->  equal to size, never changed

Für mich gibt es hier einen einzigen Schritt:

void getRidOfElementsFromWToEnd() {
    for(int i=w; i<end; ++i){
       es[i] = null;
    }
    size = w;
}

Stattdessen passiert Folgendes:

private void shiftTailOverGap(Object[] es, int w, int end) {
    System.arraycopy(es, end, es, w, size - end);
    for (int to = size, i = (size -= end - w); i < to; i++)
        es[i] = null;
}

Ich habe die Variablen hier absichtlich umbenannt.

Was ist der Sinn des Anrufs:

 System.arraycopy(es, end, es, w, size - end);

Vor allem size - end, da end es die size ganze Zeit ist - es wird nie geändert (so ist das immer zero). Dies ist hier im Grunde ein NO-OP. Welchen Eckfall vermisse ich hier?

Eugene
quelle
2
Ich habe nur einen halben Tag damit verbracht, diese Details zu verstehen, und das ist so offensichtlich, dass diese Methode auch an anderen Orten angewendet wird. Ich bin ein Idiot: |
Eugene
Ehrlich gesagt, du hast mich verwirrt. War Ihre Frage bezüglich der Verwendung System.arraycopy(es, end, es, w, size - end)als zugrunde liegende Implementierungsdetails von removeIf? Ich hatte fast das Gefühl, dazwischen eine Antwort auf eine andere Frage zu lesen. (Den Kommentar oben lesen) Ich spüre, dass es schließlich zu einer trivialen Frage kam. Ist das so?
Naman
@Naman genau, es war ungefähr so ​​gefürchtet System.arrayCopy. Trotzdem war es eine lustige Reise durch die Details (dieses interne Bit-Set hat die gleiche Idee wie java.util.BitSet)
Eugene
@Naman Wenn du willst, kannst du eine Antwort geben, wo das kein NOOP ist (Hinweis: range...) und ich werde es akzeptieren.
Eugene
1
@ Eugene in Java 8 verwendet es java.util.BitSet. Für mich BitSetsieht die Neuimplementierung der Operationen nicht wesentlich besser aus als das Original. Die Möglichkeit, ganze Wörter zu überspringen, wurde verpasst.
Holger

Antworten:

6

Sie betrachten den spezifischen (allgemeinen) Fall, dass die Liste, die Sie aufrufen, mit der Liste removeIfidentisch ist ArrayList. Nur in diesem Fall können Sie davon ausgehen, dass dies endimmer gleich ist size.

Ein Gegenbeispiel wäre:

ArrayList<Integer> l = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6, 7));
l.subList(2, 5).removeIf(i -> i%2 == 1);

Ebenso removeAllwird shiftTailOverGapmit einem endArgument aufgerufen, das sich von sizeder Anwendung auf a unterscheiden kann subList.

Eine ähnliche Situation ergibt sich, wenn Sie anrufen clear(). In diesem Fall ist die eigentliche Operation, die beim Aufrufen von ArrayListselbst ausgeführt wird, so trivial, dass die shiftTailOverGapMethode nicht einmal aufgerufen wird. Nur wenn Sie so etwas wie verwenden l.subList(a, b).clear(), wird es auf removeRange(a, b)on landen l, was wiederum, wie Sie bereits selbst herausgefunden haben, shiftTailOverGap(elementData, a, b)mit einem aufgerufen wird, bdas kleiner sein kann als size.

Holger
quelle