Java Map Interface
Schlüssel-Wert-Zuordnungen in Java mit dem Map-Interface — put, get, remove, keySet, values, entrySet.
Dieses Kapitel behandelt den Map-Vertrag: seine sieben Kernmethoden, die drei Ansichten für die Iteration, die Java-8-Standardmethoden für prägnanten modernen Map-Code, die Null-Behandlungsregeln je nach Implementierung und den Gleichheitsvergleich von Maps. Am Ende wissen Sie, welche Idiome Sie verwenden sollten und welche Standardimplementierung für eine bestimmte Aufgabe geeignet ist.
Map<K, V> ist die andere Hälfte des Collections-Frameworks. Anders als das Collection-Interface erweitert es Collection nicht — es ist eine separate Hierarchie, weil das Speichern von Schlüsseln mit zugehörigen Werten eine andere Abstraktion ist als das Speichern einer Sammlung von Elementen. Intern sind die meisten Set-Implementierungen einfach Maps, bei denen der Wert ignoriert wird — Map ist gewissermaßen die primäre Struktur und Set das einfachere Geschwister.
Der Vertrag ist kurz: Jeder Schlüssel wird höchstens einem Wert zugeordnet, die Schlüssel bilden eine Menge (keine doppelten Schlüssel) und die Werte sind eine beliebige Sammlung (doppelte Werte sind erlaubt). Was sich zwischen Implementierungen unterscheidet, sind Iterationsreihenfolge, Null-Behandlung, Sortierungsinvarianten und Thread-Sicherheit — aber die sieben Kernmethoden unten verhalten sich bei allen gleich.
Die sieben Kernmethoden
V put(K key, V value); // insert or overwrite; returns previous value or null
V get(Object key); // lookup; returns null if missing
V remove(Object key); // delete; returns previous value or null
boolean containsKey(Object k); // does the key exist (even if value is null)?
boolean containsValue(Object v); // O(n) scan of values
int size();
boolean isEmpty();Ein paar Feinheiten, die man verinnerlichen sollte:
-
putgibt den vorherigen Wert für diesen Schlüssel zurück odernull, wenn kein Mapping vorhanden war. So implementiert man "einfügen wenn abwesend"-Idiome — außer man muss das nicht, weilputIfAbsentgenau das tut und klarer ist. -
getgibtnullzurück, was entweder bedeutet "der Schlüssel ist nicht vorhanden" oder "der Schlüssel ist vorhanden, aber sein Wert istnull." Das ist eine Mehrdeutigkeit, wenn die Map Null-Werte erlaubt; verwenden SiecontainsKeyzur Unterscheidung, oder — besser —getOrDefault, um einen Standardwert bereitzustellen:int count = counts.getOrDefault("java", 0); // 0 if absent
Die drei Ansichten
Eine Map ist nicht direkt iterierbar. Um zu iterieren, fordert man eine der drei Ansichten ihres Inhalts an:
Set<K> keys = map.keySet();
Collection<V> values = map.values();
Set<Map.Entry<K, V>> es = map.entrySet();Diese Ansichten sind live — sie spiegeln Änderungen an der zugrunde liegenden Map wider, und Änderungen über die Ansicht wirken sich zurück auf die Map aus. Das Entfernen eines Eintrags über entrySet() entfernt ihn aus der Map; das Iterieren über keySet() und Aufrufen von iterator.remove() entfernt den Eintrag. Man kann nicht über keySet oder values hinzufügen (kein Wert oder Schlüssel zum Verknüpfen), aber man kann löschen oder entfernen.
Die Iteration verwendet fast immer entrySet() — beide Teile jedes Paares auf einmal zu erhalten ist günstiger als get(k) für jeden Schlüssel aufzurufen:
for (Map.Entry<String, Integer> e : counts.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue());
}Oder die in Java 8 hinzugefügte Lambda-Form:
counts.forEach((k, v) -> System.out.println(k + " -> " + v));Die wirklich wichtigen Java-8-Standardmethoden
Java 8 hat mehrere Map-Methoden hinzugefügt, die eine Funktion entgegennehmen und atomar operieren. Sie verwandeln viele dreizellige Muster in Einzeiler:
getOrDefault(k, def)—get(k), aberdefstattnull.putIfAbsent(k, v)—putnur wenn der Schlüssel fehlt.computeIfAbsent(k, fn)— berechnet atomar den Wert, wenn er fehlt, speichert ihn und gibt ihn zurück. Der Grundstein des "diesen teuren Aufruf merken"-Musters:Map<String, List<Order>> byUser = new HashMap<>(); byUser.computeIfAbsent(order.user(), u -> new ArrayList<>()).add(order);computeIfPresent(k, biFn)— berechnet nur neu, wenn der Schlüssel bereits vorhanden ist. Nützlich für Zähler, die unbekannte Schlüssel ignorieren sollen.compute(k, biFn)— universell: übergibt den aktuellen Wert (oder null) und liefert den neuen zurück. Entfernt den Eintrag, wenn die Funktion null zurückgibt.merge(k, v, biFn)— kombiniert einen neuen Wert mit dem vorhandenen, falls vorhanden. Der klassische Zähler:for (String w : words) { counts.merge(w, 1, Integer::sum); // first time: stores 1; subsequent: adds }
Das sind die Operationen, die modernes Java-Map-Handling prägnant machen. Verwenden Sie diese statt get/put-Paaren.
Null-Schlüssel und Null-Werte
Die Regeln hängen von der Implementierung ab:
| Klasse | null-Schlüssel | null-Wert |
|---|---|---|
HashMap | einer erlaubt | viele erlaubt |
LinkedHashMap | einer erlaubt | viele erlaubt |
TreeMap | nein | viele erlaubt |
Hashtable | nein | nein |
ConcurrentHashMap | nein | nein |
Map.of(...) (unveränderlich) | nein | nein |
Die allgemeine Regel für neuen Code: Speichern Sie keine Nulls in einer Map. Verwenden Sie Optional, einen Sentinelwert, oder fügen Sie den Eintrag einfach nicht ein. Die Factory Map.of erzwingt das für Sie.
Gleichheit über Implementierungen hinweg
Zwei Maps sind equals, wenn ihre entrySet()s gleich sind — gleiche Schlüssel, gleiche Werte, unabhängig von Iterationsreihenfolge oder Implementierung. Eine HashMap und eine TreeMap mit denselben Schlüssel-Wert-Paaren sind gleich. Das ist dieselbe "strukturelle Gleichheit"-Regel, die Set befolgt.
Die Standardimplementierungen auf einen Blick
| Klasse | Interne Struktur | Iterationsreihenfolge | Verwendung |
|---|---|---|---|
HashMap | Hash-Tabelle | nicht festgelegt | der Standard |
LinkedHashMap | Hash-Tabelle + verknüpfte Liste | Einfüge- oder Zugriffsreihenfolge | LRU-Caches, vorhersehbare Iteration |
TreeMap | Rot-Schwarz-Baum | nach Schlüssel sortiert | Bereichsabfragen auf Schlüsseln, sortierte Ausgabe |
Hashtable | Hash-Tabelle, synchronisiert | nicht festgelegt | veraltet; selten die richtige Wahl |
ConcurrentHashMap | segmentierte Hash-Tabelle | nicht festgelegt | mehrthreadiger Code |
EnumMap | bit-array-indiziert | Enum-Reihenfolge | Map<MyEnum, V> |
Map.of(...) | unveränderlich | nicht festgelegt | kleine feste Maps |
Die nächsten Kapitel behandeln die alltäglichen Entscheidungen im Detail: HashMap, LinkedHashMap und TreeMap. ConcurrentHashMap und EnumMap werden in späteren Teilen behandelt.
Ein ausgearbeitetes Beispiel: Zähler, Gruppierung und die drei Ansichten
Das folgende Programm zeigt die modernen Map-Idiome — merge zum Zählen, computeIfAbsent zum Gruppieren, alle drei Ansichten und den Unterschied zwischen getOrDefault und get.
Was man aus dem Ablauf mitnehmen sollte:
merge(word, 1, Integer::sum)ist das moderne, idiomatische Wortzählen. Keinget/put/null-Check nirgendwo.computeIfAbsenterstellt die leere Liste genau einmal pro Schlüssel — eine saubere Methode, um eineMap<K, List<V>>zu erstellen, ohne überallif (m.get(k) == null) m.put(k, new ArrayList<>())zu verstreuen.- Die drei Ansichten sind live Fenster in dieselbe Map;
entrySet()ist die günstigste Art zu iterieren, wenn man beide Hälften jedes Paares benötigt. getOrDefaultbeseitigt den häufigsten Grund, auf null zu prüfen. Verwenden Sie es, wann immer ein sinnvoller Standardwert vorhanden ist.- Eine
HashMapund eineTreeMapmit denselben Einträgen sindequalszueinander; das Einzige, was sich ändert, ist die Iterationsreihenfolge.
Was als nächstes kommt
Die Standardimplementierung — und diejenige, die man in 90 % des Java-Codes findet — ist Hash-Tabellen-basiert. HashMap ist das nächste Kapitel; wir behandeln das Bucket-Array, die Java-8-Treeification-Optimierung und was zu tun ist, wenn die Schlüssel eigene Klassen sind.