W3docs

Java TreeMap

TreeMap in Java nutzt einen Rot-Schwarz-Baum für sortierte Schlüssel-Wert-Zuordnungen mit NavigableMap-API.

Was eine TreeMap ist

TreeMap<K, V> ist die Map, die ihre Schlüssel sortiert hält. Intern verwendet sie einen Rot-Schwarz-Baum — denselben selbstbalancierenden binären Suchbaum, der auch TreeSet zugrunde liegt — und jede Operation ist O(log n): put, get, remove, der erste Schlüssel, der letzte Schlüssel, Bereichsabfragen. Der Preis für die logarithmische Komplexität ist die Navigations-API auf NavigableMap<K, V>: floor, ceiling, lower, higher, sub-map, head-map, tail-map, absteigende Ansichten. Nichts davon ist bei einer Hash-Tabelle ohne vollständiges Sortieren möglich.

Wenn Sie sich dabei ertappen, am Ende einer Funktion new TreeMap<>(hashMap) zu schreiben, um "die Einträge zu sortieren," ist das das Signal, dass TreeMap von Anfang an der richtige Typ gewesen wäre.

Zwei Arten, die Schlüsselreihenfolge festzulegen

Eine TreeMap muss Schlüssel irgendwie vergleichen. Wie bei TreeSet gibt es dafür zwei Möglichkeiten:

  1. Natürliche Ordnung — der Schlüsseltyp implementiert Comparable<K>. Strings, numerische Wrapper, Datumstypen und eigene record-Typen, die Comparable implementieren, kommen alle infrage.
  2. Ein Comparator<K>, der dem Konstruktor übergeben wird — verwenden Sie ihn, wenn keine natürliche Ordnung vorhanden ist oder diese nicht Ihren Anforderungen entspricht. (Siehe Comparable vs Comparator für die Unterschiede zwischen beiden.)
Map<String, Integer> byName    = new TreeMap<>();                        // case-sensitive natural
Map<String, Integer> byNameCi  = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
Map<String, Integer> reverse   = new TreeMap<>(Comparator.reverseOrder());

Wie bei TreeSet gilt: Gleichheit wird über compareTo mit Rückgabewert 0 bestimmt, nicht über equals. Zwei Schlüssel, die der Comparator als gleich einstuft, kollabieren zu einem einzigen Eintrag — das zweite put überschreibt das erste. Bei einer String.CASE_INSENSITIVE_ORDER-Schlüsselung führt das Einsetzen von "Java" und anschließend "JAVA" zu einem einzigen Eintrag, dessen Schlüssel das ursprüngliche "Java" ist und dessen Wert der Wert des zweiten put ist. Das ist fast nie das, was man aus Versehen möchte.

Die NavigableMap-API

Die Schnittstelle, die eine TreeMap implementiert, bietet Ihnen viele Operationen, die eine einfache Map nicht bietet:

TreeMap<Integer, String> t = new TreeMap<>();
t.put(10, "a"); t.put(20, "b"); t.put(30, "c"); t.put(40, "d");

t.firstKey();        // 10
t.lastKey();         // 40
t.firstEntry();      // 10=a
t.pollFirstEntry();  // 10=a, removes
t.lowerKey(30);      // 20 — strictly less
t.floorKey(30);      // 30 — ≤
t.higherKey(30);     // 40 — strictly greater
t.ceilingKey(30);    // 30 — ≥
t.headMap(30);       // {10=a, 20=b}  — keys strictly < 30
t.tailMap(30);       // {30=c, 40=d}  — keys ≥ 30
t.subMap(20, 40);    // {20=b, 30=c}  — [20, 40)
t.descendingMap();   // reverse-order view

Das ist der Grund, warum TreeMap existiert. "Finde das nächste Ereignis vor 9 Uhr" lautet headMap(9am).lastEntry() — eine logarithmische Suche. Dieselbe Operation auf einer HashMap müsste jeden Schlüssel durchsuchen.

Sub-Map-Ansichten sind live

subMap, headMap und tailMap geben Ansichten der ursprünglichen Map zurück — keine Kopien. Änderungen, die über die Ansicht vorgenommen werden, wirken sich auf die ursprüngliche Map aus, und Änderungen an der ursprünglichen Map, die in den Bereich der Ansicht fallen, erscheinen in der Ansicht. Die Ansichten erzwingen auch den Bereich: Der Versuch, über die Ansicht einen Schlüssel außerhalb des Bereichs einzufügen, löst IllegalArgumentException aus. So bleibt die bereichsbegrenzte Iteration auch bei Mutationen während des Durchlaufs sicher.

TreeMap<Integer, String> t = new TreeMap<>();
t.put(10, "a"); t.put(20, "b"); t.put(30, "c");
NavigableMap<Integer, String> mid = t.subMap(15, true, 25, true);
mid.put(20, "updated");   // OK — 20 is in range
// mid.put(40, "x");       // would throw — 40 is out of range
System.out.println(t);     // {10=a, 20=updated, 30=c}

Null wird (bei Schlüsseln) abgelehnt

In einer TreeMap kann kein null-Schlüssel vorhanden sein, aus demselben Grund, warum TreeSet sie ablehnt: Es gibt keine sinnvolle Möglichkeit, compareTo auf null aufzurufen. Das erste put(null, ...) löst NullPointerException aus. Null-Werte sind jedoch zulässig.

Wann man TreeMap wählt

Entscheidungsablauf:

  • Sie benötigen sortierte Iteration nach Schlüssel oder Bereichsabfragen auf SchlüsselnTreeMap. Die einzige Wahl.
  • Sie benötigen schnellen Zugriff und Reihenfolge ist egalHashMap. O(1) gewinnt.
  • Sie benötigen schnellen Zugriff und Iteration in EinfügereihenfolgeLinkedHashMap.
  • Der Schlüsseltyp ist ein EnumEnumMap. Schneller als TreeMap und von Natur aus geordnet.

Alle vier implementieren denselben Map-Vertrag, sodass der Wechsel zwischen ihnen in der Regel eine einzeilige Konstruktoränderung ist.

Ein nützliches Muster: Verwenden Sie eine HashMap zum schnellen Aufbau der Map und dann einmalig new TreeMap<>(hashMap), um am Ende eine sortierte Ansicht darzustellen. Schnell aufbauen, sortiert präsentieren.

Ein ausgearbeitetes Beispiel: Terminplanung, Bereichsabfragen, Comparator-Schlüsselung

Das folgende Programm verwendet eine TreeMap, um einen kleinen "Terminplan" zu modellieren, der nach Minuten seit Mitternacht als Schlüssel geordnet ist. Es demonstriert die Navigations-API für "was ist das nächste Ereignis nach X" und "alles vor Y," zeigt die live Sub-Map-Ansicht und enthüllt die Comparator-Gleichheitsfalle mit einer case-insensitiven Map.

java— editable, runs on the server

Was man aus dem Durchlauf mitnehmen sollte:

  • Der Terminplan wird in chronologischer Reihenfolge ausgegeben, ohne explizite Sortierung. Das Hinzufügen von "coffee" um 11:30 Uhr ordnete es automatisch an der richtigen Stelle ein.
  • Wir haben zum Zeitpunkt 11:00 Uhr abgefragt. ceilingEntry(11*60) gibt den nächsten Eintrag bei oder nach 11:00 Uhr zurück, das ist das Mittagessen um 13:00 Uhr (das Design-Review um 10:30 Uhr liegt vor 11:00 Uhr und kommt daher nicht infrage). lowerEntry(11*60) gibt den jüngsten Eintrag strikt vor 11:00 Uhr zurück, das Design-Review um 10:30 Uhr. Beide sind O(log n).
  • Die morning-Sub-Map ist eine live-Ansichtcoffee erschien darin, nachdem wir es in die ursprüngliche Map eingefügt hatten. Hätten wir "midnight snack" um 23:00 Uhr eingefügt, hätte die Ansicht diesen ignoriert (außerhalb des Bereichs).
  • pollFirstEntry hat wiederholt das nächste anstehende Ereignis entnommen. Das ist eine einfache Prioritätswarteschlange, wenn man auch Lookups nach Schlüssel benötigt, was eine echte PriorityQueue nicht leisten kann.
  • Die case-insensitive Map kollabierte "Java" und "JAVA" zu einem einzigen Eintrag — mit dem ursprünglichen Schlüssel "Java", aber dem Wert des zweiten puts 2. Comparator-Gleichheit, nicht equals-Gleichheit.

Was als Nächstes kommt

Die vier modernen Map-Implementierungen — HashMap, LinkedHashMap, TreeMap und ConcurrentHashMap — sind die zu bevorzugenden in neuem Code. Es gibt noch eine weitere im JDK, die all diesen vorausgeht und gelegentlich in altem Code zu sehen ist: Hashtable. Es lohnt sich zu wissen, warum sie existiert, warum sie heute fast immer die falsche Wahl ist und wie man sie ersetzen kann.

Übungen

Übung
Eine `TreeMap<Integer, String>` enthält die Schlüssel `10, 20, 30, 40`. Was gibt `tree.floorKey(25)` zurück?
Eine `TreeMap<Integer, String>` enthält die Schlüssel `10, 20, 30, 40`. Was gibt `tree.floorKey(25)` zurück?
Was this page helpful?