W3docs

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:

  • put gibt den vorherigen Wert für diesen Schlüssel zurück oder null, wenn kein Mapping vorhanden war. So implementiert man "einfügen wenn abwesend"-Idiome — außer man muss das nicht, weil putIfAbsent genau das tut und klarer ist.

  • get gibt null zurück, was entweder bedeutet "der Schlüssel ist nicht vorhanden" oder "der Schlüssel ist vorhanden, aber sein Wert ist null." Das ist eine Mehrdeutigkeit, wenn die Map Null-Werte erlaubt; verwenden Sie containsKey zur 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), aber def statt null.
  • putIfAbsent(k, v)put nur 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:

Klassenull-Schlüsselnull-Wert
HashMapeiner erlaubtviele erlaubt
LinkedHashMapeiner erlaubtviele erlaubt
TreeMapneinviele erlaubt
Hashtableneinnein
ConcurrentHashMapneinnein
Map.of(...) (unveränderlich)neinnein

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

KlasseInterne StrukturIterationsreihenfolgeVerwendung
HashMapHash-Tabellenicht festgelegtder Standard
LinkedHashMapHash-Tabelle + verknüpfte ListeEinfüge- oder ZugriffsreihenfolgeLRU-Caches, vorhersehbare Iteration
TreeMapRot-Schwarz-Baumnach Schlüssel sortiertBereichsabfragen auf Schlüsseln, sortierte Ausgabe
HashtableHash-Tabelle, synchronisiertnicht festgelegtveraltet; selten die richtige Wahl
ConcurrentHashMapsegmentierte Hash-Tabellenicht festgelegtmehrthreadiger Code
EnumMapbit-array-indiziertEnum-ReihenfolgeMap<MyEnum, V>
Map.of(...)unveränderlichnicht festgelegtkleine 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.

java— editable, runs on the server

Was man aus dem Ablauf mitnehmen sollte:

  • merge(word, 1, Integer::sum) ist das moderne, idiomatische Wortzählen. Kein get/put/null-Check nirgendwo.
  • computeIfAbsent erstellt die leere Liste genau einmal pro Schlüssel — eine saubere Methode, um eine Map<K, List<V>> zu erstellen, ohne überall if (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.
  • getOrDefault beseitigt den häufigsten Grund, auf null zu prüfen. Verwenden Sie es, wann immer ein sinnvoller Standardwert vorhanden ist.
  • Eine HashMap und eine TreeMap mit denselben Einträgen sind equals zueinander; 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.

Übungen

Übung
`counts` ist eine `HashMap<String, Integer>`. Welche Zeile ist die idiomatische Art, den Zähler für 'java' zu erhöhen, wobei ein fehlender Schlüssel als null behandelt wird?
`counts` ist eine `HashMap<String, Integer>`. Welche Zeile ist die idiomatische Art, den Zähler für 'java' zu erhöhen, wobei ein fehlender Schlüssel als null behandelt wird?
Was this page helpful?