Java TreeMap vs HashMap

1. Einführung

In diesem Artikel werden wir zwei Map -Implementierungen vergleichen:

TreeMap und HashMap .

Beide Implementierungen bilden einen integralen Bestandteil des Java Collections Framework und speichern Daten als key-value -Paare.

2. Unterschiede

2.1. Implementierung

Wir sprechen zuerst über HashMap , eine Implementierung auf Hashtabelle. Es erweitert die AbstractMap -Klasse und implementiert die Map -Schnittstelle. Eine HashMap arbeitet nach dem Prinzip von hashing .

Diese Map -Implementierung fungiert normalerweise als eine Buckash- hash-Tabelle , aber wenn Buckets zu groß werden, werden sie in Knoten von TreeNodes umgewandelt, die jeweils ähnlich wie in java.util.TreeMap.

Weitere Informationen zu den HashMap’s -Interna finden Sie unter der Artikel darauf konzentriert .

Auf der anderen Seite erweitert TreeMap die AbstractMap -Klasse und implementiert die NavigableMap -Schnittstelle. Ein TreeMap speichert Kartenelemente in einem Red-Black -Baum, der ein selbstausgleichender Binary Search Tree ist.

Weitere Informationen zu den TreeMap’s -Interna finden Sie unter link:/java-treemap

2.2. Auftrag

  • HashMap übernimmt keine Gewähr für die Anordnung der Elemente in der Map ** .

Das bedeutet, ** wir können keine Reihenfolge annehmen, während keys und values eines HashMap durchlaufen werden

@Test
public void whenInsertObjectsHashMap__thenRandomOrder() {
    Map<Integer, String> hashmap = new HashMap<>();
    hashmap.put(3, "TreeMap");
    hashmap.put(2, "vs");
    hashmap.put(1, "HashMap");

    assertThat(hashmap.keySet(), containsInAnyOrder(1, 2, 3));
}

Elemente in einer TreeMap werden jedoch nach ihrer natürlichen Reihenfolge sortiert.

Wenn TreeMap -Objekte nicht nach der natürlichen Reihenfolge sortiert werden können, können wir einen Comparator oder Comparable verwenden, um die Reihenfolge festzulegen, in der die Elemente in der Map angeordnet sind:

@Test
public void whenInsertObjectsTreeMap__thenNaturalOrder() {
    Map<Integer, String> treemap = new TreeMap<>();
    treemap.put(3, "TreeMap");
    treemap.put(2, "vs");
    treemap.put(1, "HashMap");

    assertThat(treemap.keySet(), contains(1, 2, 3));
}

2.3. Null Werte

Mit HashMap können höchstens ein null key und viele null -Werte gespeichert werden.

Sehen wir uns ein Beispiel an:

@Test
public void whenInsertNullInHashMap__thenInsertsNull() {
    Map<Integer, String> hashmap = new HashMap<>();
    hashmap.put(null, null);

    assertNull(hashmap.get(null));
}

TreeMap lässt jedoch keine null key zu, kann jedoch viele null -Werte enthalten.

Ein null -Schlüssel ist nicht zulässig, da die Methode compareTo () oder compare () eine NullPointerException auslöst:

@Test(expected = NullPointerException.class)
public void whenInsertNullInTreeMap__thenException() {
    Map<Integer, String> treemap = new TreeMap<>();
    treemap.put(null, "NullPointerException");
}
  • Wenn wir eine TreeMap mit einem benutzerdefinierten Comparator verwenden, hängt es von der Implementierung der Methode compare _ () ab, wie null_ -Werte behandelt werden. **

3. Leistungsüberprüfung

Leistung ist die kritischste Metrik, die uns hilft, die Eignung einer Datenstruktur für einen Anwendungsfall zu verstehen.

In diesem Abschnitt bieten wir eine umfassende Analyse der Leistung für HashMap und TreeMap.

3.1. HashMap

  • HashMap, eine hashtable-basierte Implementierung, verwendet intern eine Array-basierte Datenstruktur, um ihre Elemente gemäß der hash-Funktion zu organisieren. **

HashMap bietet die erwartete Leistung in konstanter Zeit O (1) für die meisten Operationen wie add () , remove () und __contains ().

Die durchschnittliche Zeit für die Suche nach einem Element unter der vernünftigen Annahme in einer Hash-Tabelle ist O (1). Eine fehlerhafte Implementierung der hash-Funktion kann jedoch zu einer schlechten Verteilung der Werte in Buckets führen, was zu folgenden Ergebnissen führt:

  • Speicher-Overhead - viele Buckets bleiben ungenutzt

  • Leistungseinbußen - Je höher die Anzahl der Kollisionen ist,

die Leistung verringern

  • Vor Java 8 war Separate Chaining die einzige bevorzugte Methode, um Kollisionen zu behandeln. ** Es wird normalerweise mit Hilfe von verknüpften Listen ( i.e. ) implementiert. Wenn eine Kollision vorliegt oder zwei verschiedene Elemente denselben Hashwert haben, speichern Sie beide Elemente in der gleiche verknüpfte Liste.

Bei der Suche nach einem Element in einer HashMap hätte im schlimmsten Fall so lange dauern können, bis nach einem Element in einer verknüpften Liste i.e. O (n) gesucht wurde.

Mit dem Auftauchen von JEP 180 hat sich jedoch eine geringfügige Änderung in der Implementierung der Anordnung der Elemente in einer HashMap ergeben. **

Gemäß der Spezifikation werden Buckets, wenn sie zu groß werden und genügend Knoten enthalten, in Modi von TreeNodes umgewandelt, die jeweils ähnlich wie die in TreeMap strukturiert sind.

  • Im Fall von hohen Hash-Kollisionen verbessert sich die Leistung im schlimmsten Fall von O (n) auf __O (log n). **

Der Code, der diese Umwandlung durchführt, wurde im Folgenden veranschaulicht:

if(binCount >= TREEIFY__THRESHOLD - 1) {
    treeifyBin(tab, hash);
}

Der Wert für TREEIFY THRESHOLD__ ist acht, was effektiv den Schwellenwert für die Verwendung eines Baums anstelle einer verknüpften Liste für einen Bucket angibt.

Es ist bewiesen, dass:

  • Eine HashMap erfordert weitaus mehr Speicher, als zum Speichern der Daten benötigt wird

  • Eine HashMap sollte nicht mehr als 70% - 75% voll sein. Wenn es nahe kommt,

es wird in der Größe verändert und die Einträge werden erneut geändert ** Das Nachwaschen erfordert n -Operationen, bei denen unsere Konstante teuer ist

Zeiteinfügung wird zur Reihenfolge O (n) ** Der Hash-Algorithmus bestimmt die Reihenfolge beim Einfügen von

Objekte in der HashMap

  • Die Leistung eines HashMap kann durch Einstellen der benutzerdefinierten Initial Capacity und des load-Faktors ** zum Zeitpunkt der Erstellung des HashMap -Objekts selbst angepasst werden.

Wir sollten jedoch eine HashMap wählen, wenn:

  • Wir wissen ungefähr, wie viele Artikel in unserer Sammlung aufbewahrt werden sollen

  • Wir möchten keine Artikel in einer natürlichen Reihenfolge extrahieren

Unter den oben genannten Umständen ist HashMap unsere beste Wahl, da es eine konstante Einfügung, Suche und Löschung von Zeit bietet.

3.2. TreeMap

Ein TreeMap speichert seine Daten in einer hierarchischen Baumstruktur mit der Möglichkeit, die Elemente mit Hilfe eines benutzerdefinierten Comparator zu sortieren.

Eine Zusammenfassung seiner Leistung:

  • TreeMap bietet für die meisten Operationen eine Leistung von O (log (n))

wie add () , remove () und contains () ** Eine Treemap kann Speicherplatz (im Vergleich zu HashMap) weil sie sparen

verwendet im Gegensatz zu a nur die Menge an Speicher, die zum Speichern der Elemente benötigt wird HashMap verwendet einen zusammenhängenden Speicherbereich ** Ein Baum sollte sein Gleichgewicht halten, um das beabsichtigte zu erhalten

Leistung, dies erfordert einen erheblichen Aufwand und erschwert somit die Implementierung

Wir sollten für eine TreeMap gehen, wann immer:

  • Speicherbeschränkungen müssen berücksichtigt werden

  • Wir wissen nicht, wie viele Elemente gespeichert werden müssen

  • Wir möchten Objekte in einer natürlichen Reihenfolge extrahieren

  • wenn Elemente konsistent hinzugefügt und entfernt werden

  • Wir sind bereit, die Suchzeit von O (log n) zu akzeptieren

4. Ähnlichkeiten

4.1. Einzigartige Elemente

Sowohl TreeMap als auch HashMap unterstützen keine doppelten Schlüssel. Falls hinzugefügt, überschreibt es das vorherige Element (ohne Fehler oder Ausnahme):

@Test
public void givenHashMapAndTreeMap__whenputDuplicates__thenOnlyUnique() {
    Map<Integer, String> treeMap = new HashMap<>();
    treeMap.put(1, "Baeldung");
    treeMap.put(1, "Baeldung");

    assertTrue(treeMap.size() == 1);

    Map<Integer, String> treeMap2 = new TreeMap<>();
    treeMap2.put(1, "Baeldung");
    treeMap2.put(1, "Baeldung");

    assertTrue(treeMap2.size() == 1);
}

4.2. Gleichzeitiger Zugriff

  • Beide Map -Implementierungen sind nicht synchronized ** und wir müssen den gleichzeitigen Zugriff selbst verwalten.

Beide müssen extern synchronisiert werden, wenn mehrere Threads gleichzeitig darauf zugreifen und mindestens einer der Threads sie ändert.

Wir müssen Collections.synchronizedMap (mapName) explizit verwenden, um eine synchronisierte Ansicht einer bereitgestellten Karte zu erhalten.

4.3. Fail-Fast-Iteratoren

Der Iterator löst eine ConcurrentModificationException aus, wenn die Map in irgendeiner Weise und zu einem Zeitpunkt geändert wird, nachdem der Iterator erstellt wurde.

Außerdem können wir die remove-Methode des Iterators verwenden, um die Map während der Iteration zu ändern.

Sehen wir uns ein Beispiel an:

@Test
public void whenModifyMapDuringIteration__thenThrowExecption() {
    Map<Integer, String> hashmap = new HashMap<>();
    hashmap.put(1, "One");
    hashmap.put(2, "Two");

    Executable executable = () -> hashmap
      .forEach((key,value) -> hashmap.remove(1));

    assertThrows(ConcurrentModificationException.class, executable);
}

5. Welche Implementierung sollte verwendet werden?

Im Allgemeinen haben beide Implementierungen ihre jeweiligen Vor- und Nachteile. Es geht jedoch darum, die zugrunde liegenden Erwartungen und Anforderungen zu verstehen, die unsere Entscheidung in Bezug auf dasselbe bestimmen müssen. **

Zusammenfassend:

  • Wir sollten eine TreeMap verwenden, wenn unsere Einträge sortiert bleiben sollen

  • Wir sollten eine HashMap verwenden, wenn die Leistung dem Speicher vorrangig ist

Verbrauch ** Da ein TreeMap eine signifikantere Lokalität hat, können wir dies in Betracht ziehen

Wenn wir auf Objekte zugreifen möchten, die relativ nahe beieinander liegen entsprechend ihrer natürlichen Reihenfolge ** HashMap kann mit initialCapacity und loadFactor abgestimmt werden.

was für die TreeMap nicht möglich ist ** Wir können LinkedHashMap verwenden, wenn Sie die Einfügungsreihenfolge beibehalten möchten

während Sie von einem ständigen Zugriff auf die Zeit profitieren

6. Fazit

In diesem Artikel haben wir die Unterschiede und Gemeinsamkeiten zwischen TreeMap und HashMap aufgezeigt.

Die Codebeispiele für diesen Artikel sind wie immer verfügbar: über GitHub .