Zeitkomplexität von Java-Sammlungen

Zeitliche Komplexität von Java-Sammlungen

1. Überblick

In diesem Tutorial werdenwe’ll talk about the performance of different collections from the Java Collection API. Wenn wir über Sammlungen sprechen, denken wir normalerweise an die DatenstrukturenList, Map, undSet und ihre gemeinsamen Implementierungen.

Zunächst werden die Einblicke in die Big-O-Komplexität für allgemeine Vorgänge untersucht und anschließend die tatsächliche Anzahl der Laufzeit einiger Erfassungsvorgänge angezeigt.

2. Zeitliche Komplexität

Normalerweisewhen we talk about time complexity, we refer to Big-O notation. Einfach ausgedrückt, beschreibt die Notation, wie die Zeit zum Ausführen des Algorithmus mit der Größe der Eingabe zunimmt.

Es stehen nützliche Beschreibungen zur Verfügung, um mehr über die Big-O-Notationtheory oder die praktischeJava examples zu erfahren.

3. List

Beginnen wir mit einer einfachen Liste - einer geordneten Sammlung.

Hier sehen wir uns eine Leistungsübersicht der Implementierungen vonArrayList, LinkedList, undCopyOnWriteArrayListan.

3.1. ArrayList

The ArrayList in Java is backed by an array. Dies hilft, die interne Logik der Implementierung zu verstehen. Eine umfassendere Anleitung fürArrayList istin this article verfügbar.

Konzentrieren wir uns zunächst auf die zeitliche Komplexität der allgemeinen Vorgänge auf hoher Ebene:

  • add() - benötigtO(1) Zeit

  • add(index, element) - läuft im Durchschnitt inO(n) Zeit

  • get() - ist immer eine konstante ZeitO(1) Operation

  • remove() - läuft in linearerO(n) Zeit. Wir müssen das gesamte Array iterieren, um das zu entfernende Element zu finden

  • *indexOf()* – läuft ebenfalls in linearer Zeit. Es durchläuft das interne Array und überprüft jedes Element einzeln. Die zeitliche Komplexität für diese Operation erfordert also immerO(n) Zeit

  • contains() - Die Implementierung basiert aufindexOf(). Es wird also auch inO(n) Zeit ausgeführt

3.2. CopyOnWriteArrayList

Diese Implementierung derList-Schnittstelle istvery useful when working with multi-threaded applications. Es ist threadsicher und wird inthis guide here gut erklärt.

Hier ist die Übersicht über die Big-O-Notation fürCopyOnWriteArrayList:

  • add() - hängt von der Position ab, an der wir Mehrwert schaffen, daher beträgt die KomplexitätO(n)

  • get() - istO(1) konstanter Zeitbetrieb

  • remove() - nimmtO(n) time

  • contains() - ebenfalls beträgt die KomplexitätO(n)

Wie wir sehen können, ist die Verwendung dieser Sammlung aufgrund der Leistungsmerkmale deradd()-Methode sehr teuer.

3.3. LinkedList

LinkedList is a linear data structure which consists of nodes holding a data field and a reference to another node. Weitere Funktionen und Fähigkeiten vonLinkedListfinden Sie unterthis article here.

Lassen Sie uns die durchschnittliche Schätzung der Zeit präsentieren, die wir für einige grundlegende Operationen benötigen:

  • add() - unterstützt das zeitkonstante Einfügen vonO(1)an jeder Position

  • get() - Die Suche nach einem Element dauertO(n) 

  • remove() - Das Entfernen eines Elements erfordert auch die Operation vonO(1), da wir die Position des Elements angeben

  • contains() - hat auch die zeitliche Komplexität vonO(n)

3.4. Aufwärmen der JVM

Um die Theorie zu beweisen, spielen wir nun mit den tatsächlichen Daten. To be more precise, we’ll present the JMH (Java Microbenchmark Harness) test results of the most common collection operations.

Wenn Sie mit dem JMH-Tool nicht vertraut sind, lesen Sie dieseuseful guide.

Zunächst stellen wir die wichtigsten Parameter unserer Benchmark-Tests vor:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 10)
public class ArrayListBenchmark {
}

Dann setzen wir die Anzahl der Aufwärmiterationen auf10. Außerdem möchten wir die durchschnittliche Laufzeit unserer Ergebnisse in Mikrosekunden anzeigen.

3.5. Benchmark-Tests

Jetzt ist es Zeit, unsere Leistungstests durchzuführen. Zuerst beginnen wir mitArrayList:

@State(Scope.Thread)
public static class MyState {

    List employeeList = new ArrayList<>();

    long iterations = 100000;

    Employee employee = new Employee(100L, "Harry");

    int employeeIndex = -1;

    @Setup(Level.Trial)
    public void setUp() {
        for (long i = 0; i < iterations; i++) {
            employeeList.add(new Employee(i, "John"));
        }

        employeeList.add(employee);
        employeeIndex = employeeList.indexOf(employee);
    }
}

Innerhalb unsererArrayListBenchmark fügen wir dieState-Klasse hinzu, um die Anfangsdaten zu speichern.

Hier erstellen wirArrayList vonEmployee Objekten. Nachwe initialize it with 100.000 items inside of the setUp() method. The @State indicates that the @Benchmark tests have full access to the variables declared in it within the same thread.

Schließlich ist es an der Zeit, die Benchmark-Tests für die Methodenadd(), contains(), indexOf(), remove(), undget()hinzuzufügen:

@Benchmark
public void testAdd(ArrayListBenchmark.MyState state) {
    state.employeeList.add(new Employee(state.iterations + 1, "John"));
}

@Benchmark
public void testAddAt(ArrayListBenchmark.MyState state) {
    state.employeeList.add((int) (state.iterations), new Employee(state.iterations, "John"));
}

@Benchmark
public boolean testContains(ArrayListBenchmark.MyState state) {
    return state.employeeList.contains(state.employee);
}

@Benchmark
public int testIndexOf(ArrayListBenchmark.MyState state) {
    return state.employeeList.indexOf(state.employee);
}

@Benchmark
public Employee testGet(ArrayListBenchmark.MyState state) {
    return state.employeeList.get(state.employeeIndex);
}

@Benchmark
public boolean testRemove(ArrayListBenchmark.MyState state) {
    return state.employeeList.remove(state.employee);
}

3.6. Testergebnisse

Alle Ergebnisse sind in Mikrosekunden angegeben:

Benchmark                        Mode  Cnt     Score     Error
ArrayListBenchmark.testAdd       avgt   20     2.296 ±   0.007
ArrayListBenchmark.testAddAt     avgt   20   101.092 ±  14.145
ArrayListBenchmark.testContains  avgt   20   709.404 ±  64.331
ArrayListBenchmark.testGet       avgt   20     0.007 ±   0.001
ArrayListBenchmark.testIndexOf   avgt   20   717.158 ±  58.782
ArrayListBenchmark.testRemove    avgt   20   624.856 ±  51.101

From the results we can learn, that testContains() and testIndexOf() methods run in approximately the same time. Wir können auch deutlich den großen Unterschied zwischen den Methodenwerten vontestAdd(), testGet()und den übrigen Ergebnissen erkennen. Das Hinzufügen eines Elements dauert 2.296Mikrosekunden und das Erhalten eines Elements dauert 0,007 Mikrosekunden.

Das Suchen oder Entfernen eines Elements kostet ungefähr700Mikrosekunden. Diese Zahlen sind der Beweis für den theoretischen Teil, in dem wir gelernt haben, dassadd(), undget()O(1) Zeitkomplexität haben und die anderen MethodenO(n) sind. n=10.000 Elemente in unserem Beispiel.

Ebenso können wir dieselben Tests für die Sammlung vonCopyOnWriteArrayListchreiben. Wir müssen lediglich dieArrayList in employeeList durch dieCopyOnWriteArrayList-Instanz ersetzen.

Hier sind die Ergebnisse des Benchmarktests:

Benchmark                          Mode  Cnt    Score     Error
CopyOnWriteBenchmark.testAdd       avgt   20  652.189 ±  36.641
CopyOnWriteBenchmark.testAddAt     avgt   20  897.258 ±  35.363
CopyOnWriteBenchmark.testContains  avgt   20  537.098 ±  54.235
CopyOnWriteBenchmark.testGet       avgt   20    0.006 ±   0.001
CopyOnWriteBenchmark.testIndexOf   avgt   20  547.207 ±  48.904
CopyOnWriteBenchmark.testRemove    avgt   20  648.162 ± 138.379

Auch hier bestätigen die Zahlen die Theorie. Wie wir sehen können, läufttestGet() im Durchschnitt in 0,006 ms, was wir alsO(1) betrachten können. Comparing to ArrayList, we also notice the significant difference between testAdd() method results. As we have here O(n) complexity for the add() method versus ArrayList’s O(1). 

We can clearly see the linear growth of the time, as performance numbers are 878.166 compared to 0.051.

Jetzt ist esLinkedListZeit:

Benchmark        Cnt     Score       Error
testAdd          20     2.580        ± 0.003
testContains     20     1808.102     ± 68.155
testGet          20     1561.831     ± 70.876
testRemove       20     0.006        ± 0.001

Wir können den Bewertungen entnehmen, dass das Hinzufügen und Entfernen von Elementen inLinkedList ziemlich schnell ist.

Darüber hinaus besteht eine erhebliche Leistungslücke zwischen dem Hinzufügen / Entfernen und dem Abrufen / Enthalten von Vorgängen.

4. Map

Mit den neuesten JDK-Versionen stellen wir eine signifikante Leistungsverbesserung fürMap-Implementierungen fest, z. B. das Ersetzen vonLinkedList durch die ausgeglichene Baumknotenstruktur inHashMap, LinkedHashMap internalen Implementierungen. This shortens the element lookup worst-case scenario from O(n) to O(log(n)) time during the HashMap collisions.

Wenn wir jedoch die richtigen Methoden.equals() und.hashcode()implementieren, sind Kollisionen unwahrscheinlich.

Weitere Informationen zuHashMap-Kollisionen finden Sie unterthis write-up. From the write-up, we can also learn, that storing and retrieving elements from the HashMap takes constant O(1) time.

4.1. Testen vonO(1) Operationen

Lassen Sie uns einige tatsächliche Zahlen zeigen. Erstens für dieHashMap:

Benchmark                         Mode  Cnt  Score   Error
HashMapBenchmark.testContainsKey  avgt   20  0.009 ± 0.002
HashMapBenchmark.testGet          avgt   20  0.011 ± 0.001
HashMapBenchmark.testPut          avgt   20  0.019 ± 0.002
HashMapBenchmark.testRemove       avgt   20  0.010 ± 0.001

As we see, the numbers prove the O(1) constant time for running the methods listed above. Lassen Sie uns nun die Testergebnisse vonHashMapmit den Instanzergebnissen der anderenMapvergleichen.

Für alle aufgelisteten Methodenwe have O(1) for HashMap, LinkedHashMap, IdentityHashMap, WeakHashMap, EnumMap and ConcurrentHashMap.

Lassen Sie uns die Ergebnisse der verbleibenden Testergebnisse in Form einer Tabelle präsentieren:

Benchmark      LinkedHashMap  IdentityHashMap  WeakHashMap  ConcurrentHashMap
testContainsKey    0.008         0.009          0.014          0.011
testGet            0.011         0.109          0.019          0.012
testPut            0.020         0.013          0.020          0.031
testRemove         0.011         0.115          0.021          0.019

Aus den Ausgabenummern können wir die Behauptungen der Zeitkomplexität vonO(1)bestätigen.

4.2. Testen vonO(log(n)) Operationen

Für die BaumstrukturTreeMap and ConcurrentSkipListMap the put(), get(), remove(), containsKey()  operations time is O(log(n)).

[.pl-smi] #Hier,we want to make sure that our performance tests will run approximately in logarithmic time. Aus diesem Grund initialisieren wir die Karten kontinuierlich mit n=1000, 10,000, 100,000, 1,000,000 Elementen. #

In diesem Fall interessiert uns die Gesamtausführungszeit:

items count (n)         1000      10,000     100,000   1,000,000
all tests total score   00:03:17  00:03:17   00:03:30  00:05:27

[.pl-smi] #Wennn=1000 die Gesamtausführungszeit von00:03:17 Millisekunden beträgt. n=10,000 die Zeit ist fast unverändert00:03:18 ms. n=100,000 hat geringfügigen Anstieg00:03:30. Und schließlich, wennn=1,000,000 ist, wird der Lauf in00:05:27 ms abgeschlossen. #

Nach dem Vergleich der Laufzeitzahlen mit derlog(n)-Funktion von jedemn können wir bestätigen, dass die Korrelation beider Funktionen übereinstimmt.

5. Set

Im Allgemeinen istSet eine Sammlung eindeutiger Elemente. Hier werden wir die Implementierungen vonHashSet,LinkedHashSet,EnumSet, TreeSet, CopyOnWriteArraySet, undConcurrentSkipListSet derSet-Schnittstelle untersuchen.

Um die Interna derHashSet besser zu verstehen, istthis guide hier, um zu helfen.

Lassen Sie uns nun die Zahlen zur Zeitkomplexität präsentieren. For HashSetLinkedHashSet, and EnumSet the add(), remove() and contains() operations cost constant O(1) time. Thanks to the internal HashMap implementation.

Ebenso die TreeSet has O(log(n)) time complexity für die für die vorherige Gruppe aufgelisteten Operationen. Dies liegt an der Implementierung vonTreeMap. Die Zeitkomplexität fürConcurrentSkipListSet ist auchO(log(n)) Zeit, da sie auf der Datenstruktur der Überspringliste basiert.

FürCopyOnWriteArraySet, haben die Methodenadd(), remove() andcontains() eine durchschnittliche zeitliche Komplexität von O (n).

5.1. Testmethoden

Kommen wir nun zu unseren Benchmark-Tests:

@Benchmark
public boolean testAdd(SetBenchMark.MyState state) {
    return state.employeeSet.add(state.employee);
}

@Benchmark
public Boolean testContains(SetBenchMark.MyState state) {
    return state.employeeSet.contains(state.employee);
}

@Benchmark
public boolean testRemove(SetBenchMark.MyState state) {
    return state.employeeSet.remove(state.employee);
}

Darüber hinaus lassen wir die verbleibenden Benchmark-Konfigurationen unverändert.

5.2. Zahlen vergleichen

Sehen wir uns das Verhalten der Laufzeitausführungsbewertung fürHashSet undLinkedHashSet an, dien = 1000; 10,000; 100,000 Elemente rasieren.

Für dieHashSet,  sind die Zahlen:

Benchmark      1000    10,000    100,000
.add()         0.026   0.023     0.024
.remove()      0.009   0.009     0.009
.contains()    0.009   0.009     0.010

In ähnlicher Weise sind die Ergebnisse fürLinkedHashSet:

Benchmark      1000    10,000    100,000
.add()         0.022   0.026     0.027
.remove()      0.008   0.012     0.009
.contains()    0.008   0.013     0.009

Wie wir sehen, bleiben die Werte für jede Operation nahezu gleich. Wenn wir sie mit den Testausgaben vonHashMapvergleichen, sehen sie sogar gleich aus.

Als Ergebnis bestätigen wir, dass alle getesteten Methoden in konstanterO(1)-Zeit ausgeführt werden.

6. Fazit

In diesem Artikel werdenwe present the time complexity of the most common implementations of the Java data structures.

Separat zeigen wir die tatsächliche Laufzeitleistung für jeden Sammlungstyp durch die JVM-Benchmarktests an. Wir haben auch die Leistung der gleichen Operationen in verschiedenen Sammlungen verglichen. Als Ergebnis lernen wir, die richtige Kollektion auszuwählen, die unseren Bedürfnissen entspricht.

Wie üblich ist der vollständige Code für diesen Artikel inover on GitHub verfügbar.