Jon Skeet hat kürzlich in seinem Blog ein interessantes Programmierthema angesprochen: "Es gibt ein Loch in meiner Abstraktion, liebe Liza, liebe Liza" (Hervorhebung hinzugefügt):
Ich habe ein Set - a
HashSet
, in der Tat. Ich möchte einige Elemente daraus entfernen ... und viele der Elemente sind möglicherweise nicht vorhanden. In unserem Testfall befindet sich keines der Elemente in der Sammlung "Umzüge" im Originalsatz. Das klingt - und ist - extrem einfach zu codieren. Wir müssen uns dochSet<T>.removeAll
helfen, oder?Wir geben die Größe des "Quell" -Satzes und die Größe der "Entfernungs" -Sammlung in der Befehlszeile an und erstellen beide. Der Quellensatz enthält nur nicht negative Ganzzahlen. Der Entfernungssatz enthält nur negative Ganzzahlen. Wir messen, wie lange es dauert, alle Elemente mit zu entfernen.
System.currentTimeMillis()
Dies ist nicht die genaueste Stoppuhr der Welt, aber in diesem Fall mehr als ausreichend, wie Sie sehen werden. Hier ist der Code:import java.util.*; public class Test { public static void main(String[] args) { int sourceSize = Integer.parseInt(args[0]); int removalsSize = Integer.parseInt(args[1]); Set<Integer> source = new HashSet<Integer>(); Collection<Integer> removals = new ArrayList<Integer>(); for (int i = 0; i < sourceSize; i++) { source.add(i); } for (int i = 1; i <= removalsSize; i++) { removals.add(-i); } long start = System.currentTimeMillis(); source.removeAll(removals); long end = System.currentTimeMillis(); System.out.println("Time taken: " + (end - start) + "ms"); } }
Beginnen wir mit einer einfachen Aufgabe: einem Quellensatz von 100 Elementen und 100 zu entfernenden Elementen:
c:UsersJonTest>java Test 100 100 Time taken: 1ms
Okay, wir hatten also nicht erwartet, dass es langsam wird ... klar, wir können die Dinge ein wenig beschleunigen. Wie wäre es mit einer Quelle von einer Million Gegenständen und 300.000 zu entfernenden Gegenständen?
c:UsersJonTest>java Test 1000000 300000 Time taken: 38ms
Hmm. Das scheint immer noch ziemlich schnell zu sein. Jetzt fühle ich mich ein bisschen grausam und habe es gebeten, all das zu entfernen. Machen wir es uns etwas einfacher - 300.000 Quellelemente und 300.000 Entfernungen:
c:UsersJonTest>java Test 300000 300000 Time taken: 178131ms
Entschuldigen Sie? Fast drei Minuten ? Huch! Sicherlich sollte es einfacher sein, Gegenstände aus einer kleineren Sammlung zu entfernen als die, die wir in 38 ms verwaltet haben?
Kann jemand erklären, warum dies geschieht? Warum ist die HashSet<T>.removeAll
Methode so langsam?
quelle
Antworten:
Das Verhalten ist (etwas) im Javadoc dokumentiert :
Was dies in der Praxis bedeutet, wenn Sie anrufen
source.removeAll(removals);
:Wenn die
removals
Sammlung kleiner als istsource
, wird dieremove
MethodeHashSet
aufgerufen, die schnell ist.Wenn die
removals
Sammlung gleich oder größer als die istsource
,removals.contains
wird aufgerufen, was für eine ArrayList langsam ist.Schnelle Lösung:
Beachten Sie, dass es einen offenen Fehler gibt , der dem, was Sie beschreiben, sehr ähnlich ist. Das Fazit scheint zu sein, dass es wahrscheinlich eine schlechte Wahl ist, aber nicht geändert werden kann, da es im Javadoc dokumentiert ist.
Als Referenz ist dies der Code von
removeAll
(in Java 8 - andere Versionen wurden nicht überprüft):quelle
ArrayList#contains
der Schuldige war. Ein Blick auf den Code vonAbstractSet#removeAll
gab den Rest der Antwort.