Java TreeMap vs HashMap

1. Einleitung

In diesem Artikel werden zwei Map- Implementierungen verglichen : TreeMap und HashMap .

Beide Implementierungen sind integraler Bestandteil des Java Collections Framework und speichern Daten als Schlüssel-Wert- Paare.

2. Unterschiede

2.1. Implementierung

Wir werden zuerst über die HashMap sprechen, die eine auf Hashtabellen basierende Implementierung ist. Es erweitert die AbstractMap- Klasse und implementiert die Map- Schnittstelle. Eine HashMap arbeitet nach dem Prinzip des Hashings .

Diese Map- Implementierung fungiert normalerweise als Bucket- Hash-Tabelle . Wenn Buckets jedoch zu groß werden, werden sie in Knoten von TreeNodes umgewandelt , die jeweils ähnlich wie in java.util.TreeMap strukturiert sind .

Weitere Informationen zu den Interna der HashMap finden Sie in dem Artikel, der sich darauf konzentriert.

Andererseits erweitert TreeMap die AbstractMap- Klasse und implementiert die NavigableMap- Schnittstelle. Ein TreeMap speichern Kartenelemente in einem Rot-Schwarz - Baum, der eine ist Selbst Balancing binären Suchbaumes .

Weitere Informationen zu den Interna der TreeMap finden Sie in dem Artikel, der sich hier darauf konzentriert.

2.2. Auftrag

HashMap bietet keine Garantie für die Anordnung der Elemente in der Karte .

Dies bedeutet, dass wir beim Durchlaufen von Schlüsseln und Werten einer HashMap keine Reihenfolge annehmen können :

@Test public void whenInsertObjectsHashMap_thenRandomOrder() { Map 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 natürlicher Reihenfolge sortiert werden können, können wir einen Komparator oder Vergleichbaren verwenden , um die Reihenfolge zu definieren, in der die Elemente in der Karte angeordnet sind :

@Test public void whenInsertObjectsTreeMap_thenNaturalOrder() { Map 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

HashMap erlaubt höchstens eine Speicherung von null Schlüssel und viele Nullwerte.

Sehen wir uns ein Beispiel an:

@Test public void whenInsertNullInHashMap_thenInsertsNull() { Map hashmap = new HashMap(); hashmap.put(null, null); assertNull(hashmap.get(null)); }

Allerdings TreeMap keinen erlauben null Schlüssel aber viele enthalten Nullwerte.

Ein Nulltaste ist , weil das nicht erlaubt compareTo () oder das Vergleichen () Methode löst eine Nullpointer:

@Test(expected = NullPointerException.class) public void whenInsertNullInTreeMap_thenException() { Map treemap = new TreeMap(); treemap.put(null, "NullPointerException"); }

Wenn wir eine TreeMap mit einem benutzerdefinierten Komparator verwenden , hängt es von der Implementierung der compare () -Methode ab, wie Nullwerte behandelt werden.

3. Leistungsanalyse

Die 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 von HashMap und TreeMap.

3.1. HashMap

HashMap ist eine auf Hashtabellen basierende Implementierung und verwendet intern eine Array-basierte Datenstruktur, um ihre Elemente gemäß der Hash-Funktion zu organisieren .

HashMap bietet die erwartete zeitkonstante Leistung O (1) für die meisten Operationen wie add () , remove () und enthält (). Daher ist es deutlich schneller als eine TreeMap .

Die durchschnittliche Zeit für die Suche nach einem Element unter der vernünftigen Annahme in einer Hash-Tabelle beträgt O (1). Eine fehlerhafte Implementierung der Hash-Funktion kann jedoch zu einer schlechten Werteverteilung in Buckets führen, was zu Folgendem führt:

  • Speicheraufwand - viele Buckets bleiben unbenutzt
  • Leistungsabfall - Je höher die Anzahl der Kollisionen, desto geringer die Leistung

Vor Java 8 war die separate Verkettung die einzig bevorzugte Methode zur Behandlung von Kollisionen. Es ist in der Regel verkettete Listen implementiert, dh , wenn es eine Kollision oder zwei verschiedene Elemente haben denselben Hashwert beide dann speichern Sie die Elemente in der gleichen verknüpften Liste.

Daher hätte die Suche nach einem Element in einer HashMap im schlimmsten Fall so lange dauern können wie die Suche nach einem Element in einer verknüpften Liste, dh O (n) Zeit.

Mit dem Erscheinen von JEP 180 hat sich jedoch die Implementierung der Anordnung der Elemente in einer HashMap geringfügig geändert.

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

Daher verbessert sich im Falle hoher Hash-Kollisionen die Worst-Case-Leistung von O (n) auf O (log n).

Der Code, der diese Transformation ausführt, wurde unten dargestellt:

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

Der Wert für TREEIFY_THRESHOLD ist acht, was effektiv die Schwellenwertanzahl für die Verwendung eines Baums anstelle einer verknüpften Liste für einen Bucket angibt .

Es ist bewiesen, dass:

  • Eine HashMap benötigt viel mehr Speicher als zum Speichern ihrer Daten erforderlich ist
  • Eine HashMap sollte nicht mehr als 70% - 75% voll sein. Wenn es nahe kommt, wird die Größe geändert und die Einträge erneut aufbereitet
  • Das Wiederaufwärmen erfordert n Operationen, was kostspielig ist, wobei unser Einfügen mit konstanter Zeit in der Größenordnung O (n) liegt.
  • Es ist der Hashing-Algorithmus, der die Reihenfolge des Einfügens der Objekte in die HashMap bestimmt

The performance of a HashMap can be tuned by setting the custom initial capacity and the load factor, at the time of HashMap object creation itself.

However, we should choose a HashMap if:

  • we know approximately how many items to maintain in our collection
  • we don't want to extract items in a natural order

Under the above circumstances, HashMap is our best choice because it offers constant time insertion, search, and deletion.

3.2. TreeMap

A TreeMap stores its data in a hierarchical tree with the ability to sort the elements with the help of a custom Comparator.

A summary of its performance:

  • TreeMap provides a performance of O(log(n)) for most operations like add(), remove() and contains()
  • A Treemap can save memory (in comparison to HashMap) because it only uses the amount of memory needed to hold its items, unlike a HashMap which uses contiguous region of memory
  • A tree should maintain its balance in order to keep its intended performance, this requires a considerable amount of effort, hence complicates the implementation

We should go for a TreeMap whenever:

  • memory limitations have to be taken into consideration
  • we don't know how many items have to be stored in memory
  • we want to extract objects in a natural order
  • if items will be consistently added and removed
  • we're willing to accept O(log n) search time

4. Similarities

4.1. Unique Elements

Both TreeMap and HashMap don't support duplicate keys. If added, it overrides the previous element (without an error or an exception):

@Test public void givenHashMapAndTreeMap_whenputDuplicates_thenOnlyUnique() { Map treeMap = new HashMap(); treeMap.put(1, "Baeldung"); treeMap.put(1, "Baeldung"); assertTrue(treeMap.size() == 1); Map treeMap2 = new TreeMap(); treeMap2.put(1, "Baeldung"); treeMap2.put(1, "Baeldung"); assertTrue(treeMap2.size() == 1); }

4.2. Concurrent Access

Both Map implementations aren't synchronized and we need to manage concurrent access on our own.

Both must be synchronized externally whenever multiple threads access them concurrently and at least one of the threads modifies them.

We have to explicitly use Collections.synchronizedMap(mapName) to obtain a synchronized view of a provided map.

4.3. Fail-Fast Iterators

The Iterator throws a ConcurrentModificationException if the Map gets modified in any way and at any time once the iterator has been created.

Additionally, we can use the iterator’s remove method to alter the Map during iteration.

Let's see an example:

@Test public void whenModifyMapDuringIteration_thenThrowExecption() { Map 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. Which Implementation to Use?

In general, both implementations have their respective pros and cons, however, it's about understanding the underlying expectation and requirement which must govern our choice regarding the same.

Summarizing:

  • We should use a TreeMap if we want to keep our entries sorted
  • We should use a HashMap if we prioritize performance over memory consumption
  • Da eine TreeMap eine bedeutendere Lokalität hat, können wir dies in Betracht ziehen, wenn wir auf Objekte zugreifen möchten, die gemäß ihrer natürlichen Reihenfolge relativ nahe beieinander liegen
  • HashMap kann mit initialCapacity und loadFactor optimiert werden , was für TreeMap nicht möglich ist
  • Wir können die LinkedHashMap verwenden, wenn wir die Einfügereihenfolge beibehalten und gleichzeitig von einem konstanten Zeitzugriff profitieren möchten

6. Fazit

In diesem Artikel haben wir die Unterschiede und Ähnlichkeiten zwischen TreeMap und HashMap gezeigt .

Wie immer sind die Codebeispiele für diesen Artikel auf GitHub verfügbar.