W3docs

Java HashMap

Nutze die hash-tabellenbasierte HashMap für schnelle, ungeordnete Schlüssel-Wert-Suchen in Java.

HashMap<K, V> ist die Standard-Map-Implementierung im JDK und die am häufigsten verwendete Datenstruktur in Java-Anwendungscode. Sie bildet die Grundlage für HashSet (das ein HashMap ist, bei dem alle Werte auf ein einziges Dummy-Objekt gesetzt sind), wird von Collectors.toMap erzeugt und steht hinter jeder "Lookup-Tabelle", die du schreibst und die weder sortiert noch concurrent ist. Operationen sind erwartet O(1) — ein Hash, ein Bucket-Index, ein oder zwei equals-Prüfungen — unabhängig von der Größe.

Kernoperationen

Das sind die Methoden, die du täglich verwendest. Jede einzelne ist erwartet O(1).

MethodeWas sie tut
put(k, v)Fügt ein oder überschreibt; gibt den vorherigen Wert zurück (oder null).
get(k)Gibt den Wert zurück, oder null wenn der Schlüssel fehlt.
getOrDefault(k, def)Wie get, gibt aber def statt null bei einem Fehlschlag zurück.
putIfAbsent(k, v)Setzt den Wert nur, wenn der Schlüssel fehlt oder auf null abgebildet ist.
merge(k, v, fn)Kombiniert einen vorhandenen Wert mit v über fn — das Counter-Idiom.
computeIfAbsent(k, fn)Berechnet und speichert einen Wert bei einem Fehlschlag — das Cache-Idiom.
remove(k)Entfernt den Eintrag; gibt den entfernten Wert zurück, oder null.
containsKey(k)Der einzig zuverlässige Weg, "abwesend" von "auf null abgebildet" zu unterscheiden.

Iteriere über Einträge (nicht Schlüssel, wenn du auch die Werte benötigst), um eine zweite Suche pro Schlüssel zu vermeiden:

Map<String, Integer> scores = new HashMap<>();
scores.put("alice", 90);
scores.put("bob", 75);

for (Map.Entry<String, Integer> e : scores.entrySet()) {
    System.out.println(e.getKey() + " -> " + e.getValue());
}

// Or the lambda form:
scores.forEach((name, score) -> System.out.println(name + " -> " + score));

Wie die Tabelle aufgebaut ist

Ein HashMap hält ein Array von Buckets mit Potenz-von-zwei-Größe. Das Einfügen eines Eintrags führt fünf Schritte aus:

  1. Berechne h = hashCode(key). Mische die oberen und unteren 16 Bits — h ^ (h >>> 16) — damit ein Hash wie 0x12340000 seine oberen Bits beim Maskieren nicht verliert.
  2. Maske: i = h & (table.length - 1). Das ist h mod length für Potenz-von-zwei-Längen und schneller als der Modulo-Operator.
  3. Durchlaufe die Kette bei table[i]. Falls ein Knoten mit einem gleichen Schlüssel existiert, überschreibe seinen Wert und gib den alten zurück.
  4. Andernfalls wird ein neuer Knoten vorangestellt (oder seit Java 8 angehängt).
  5. Falls size > capacity * loadFactor, wird die Größe angepasst: Die Tabelle verdoppelt sich und jeder Eintrag wird neu verteilt.

Bis Java 7 war die Bucket-Kette eine einfach verkettete Liste, punkt. Ab Java 8 wird der Bucket, sobald eine Kette acht Einträge erreicht, in einen kleinen balancierten Baum (einen Rot-Schwarz-Baum) umgewandelt, der nach dem Hash geordnet ist. Die Suche in diesem Bucket wird O(log n) statt O(n), was den Schaden eines Denial-of-Service-Angriffs begrenzt, der kollidierende Hashes erzeugt. Wenn der Baum auf sechs oder weniger Einträge schrumpft, wird er wieder zur Liste. Im normalen Code wirst du das nicht sehen — es ist nur relevant, wenn dein hashCode adversarisch oder pathologisch schlecht ist.

Kapazität, Ladefaktor und Vorinitialisierung

Dieselben Parameter wie bei HashSet:

  • Anfangskapazität — Standard 16, aufgerundet auf eine Zweierpotenz.
  • Ladefaktor — Standard 0,75. Wenn size > capacity * 0.75, verdoppelt sich die Tabelle.

Wenn du die Größe im Voraus kennst, initialisiere vor:

Map<String, User> users = new HashMap<>(expectedSize * 4 / 3); // skip the doublings

Oder seit Java 19 die explizite Factory:

Map<String, User> users = HashMap.newHashMap(expectedSize);

Das ist der sauberste Ausdruck der Absicht — es berechnet die richtige Anfangskapazität aus einer Zielgröße, sodass die Tabelle nicht wachsen muss.

Null-Schlüssel und Null-Werte

HashMap erlaubt einen null-Schlüssel (er wird in Bucket 0 mit Hash 0 gespeichert) und eine beliebige Anzahl von null-Werten. Das ist eine Erleichterung gegenüber Hashtable (das beides ablehnt), trübt aber die Bedeutung von get(k) == null:

m.put("key", null);
m.get("key");          // returns null
m.containsKey("key"); // returns true

Die Kosten der Mehrdeutigkeit sind real. Bevorzuge es, keine null-Werte zu speichern; verwende Optional, einen Sentinel-Wert oder lass den Schlüssel einfach weg. Die Java-9+-Factory Map.of(...) erzwingt das für dich.

hashCode und equals sind dein Vertrag

Das Einfügen einer eigenen Klasse in eine HashMap funktioniert nur, wenn hashCode und equals konsistent sind. Die gleichen Regeln wie bei HashSet:

  • Gleiche Objekte müssen gleiche Hash-Codes haben.
  • Ungleiche Objekte dürfen kollidieren (das ist in Ordnung, deshalb sind Buckets Ketten).
  • Das Mutieren eines Schlüssels nach dem Einfügen ist undefiniertes Verhalten.

Verwende einen record wenn möglich — beide Methoden werden korrekt generiert. Oder lass die IDE sie generieren. Schreibe hashCode niemals von Hand, wenn du es vermeiden kannst.

record UserId(String tenant, String localPart) {}
Map<UserId, User> directory = new HashMap<>();
directory.put(new UserId("acme", "alice"), new User(/*...*/));
directory.get(new UserId("acme", "alice")); // hit

Iterationsreihenfolge — explizit undefiniert

HashMap macht keine Garantie über die Iterationsreihenfolge. Die Reihenfolge hängt vom Bucket-Layout ab, das wiederum vom Hash, der Kapazität und der Resize-Historie abhängt — sie kann sich zwischen Ausführungen und zwischen JVM-Versionen ändern. Wenn du dich auf die Reihenfolge verlässt, ist dein Code fehlerhaft; wenn deine Tests sich auf die Reihenfolge verlassen, sind sie instabil.

Wenn die Iterationsreihenfolge wichtig ist, verwende LinkedHashMap für Einfügereihenfolge oder TreeMap für sortierte Reihenfolge. Beide sind direkte Ersetzungen.

Nicht thread-sicher

HashMap beschädigt sich selbst bei gleichzeitiger Mutation — und historisch gesehen war ein berüchtigter Fehlerfall eine Endlosschleife während eines gleichzeitigen Resize. Teile eine HashMap nicht zwischen Threads. Die richtige Struktur für Multi-Thread-Code ist ConcurrentHashMap (später im Concurrency-Teil behandelt). Collections.synchronizedMap(new HashMap<>()) existiert, verwendet aber ein einziges Lock für jede Operation, was langsamer und selten die richtige Antwort ist.

Ein ausgearbeitetes Beispiel: Counter, Lookup-Tabelle und die modernen Idiome

Das folgende Programm verwendet eine HashMap auf mehrere Arten: einen Wortzähler über merge, einen rekursiven Memoization-Cache, eine Demonstration der Null-Wert-Mehrdeutigkeit, die Java-19-Factory newHashMap und einen Record als zusammengesetzten Schlüssel.

java— editable, runs on the server

Was aus dem Lauf mitzunehmen ist:

  • merge reduziert den dreistufigen Ablauf "get, Standard-oder-addiere-eins, put" auf einen einzigen Aufruf. Verwende es immer, wenn du einen pro-Schlüssel-Counter oder eine Summe pflegst.
  • Der Fibonacci-Cache verwandelt eine exponentielle Rekursion in eine lineare: Prüfe die Map, rekursiere bei einem Fehlschlag, dann put das Ergebnis. Beachte, dass es get + put statt computeIfAbsent verwendet — ein rekursives computeIfAbsent mutiert die Map, während seine eigene Mapping-Funktion noch läuft, und seit Java 9 wirft das eine ConcurrentModificationException. Reserviere computeIfAbsent für nicht-rekursive "Lade-oder-Berechne"-Suchen.
  • Die Null-Mehrdeutigkeit ist real. get gab auf die gleiche Weise null für einen vorhandenen und einen fehlenden Schlüssel zurück. Der einzige Weg, sie zu unterscheiden, ist containsKey — oder indem man entscheidet, keine Nulls zu speichern.
  • Die Vorinitialisierung mit HashMap.newHashMap(1_000_000) lässt eine Million Einfügungen ohne jegliche Rehashes abschließen — die Tabelle startet mit der richtigen Kapazität.
  • Der UserId-Record gibt korrektes equals/hashCode kostenlos. Das ist der moderne Weg, HashMap-Schlüssel aus mehreren Feldern zusammenzusetzen.

Was kommt als Nächstes

HashMap verspricht keine Iterationsreihenfolge. Wenn du die Einfügereihenfolge gespeichert haben möchtest — zum Beispiel wenn du die Map in JSON serialisierst und eine stabile Ausgabe willst — ist das richtige Werkzeug LinkedHashMap. Es ist auch die Grundlage eines Lehrbuch-LRU-Caches, den wir im selben Kapitel behandeln.

Übungen

Übung
Du siehst `m.merge(key, 1, Integer::sum)` in Code, wobei `m` ein `Map<String, Integer>` ist. Was macht es?
Du siehst `m.merge(key, 1, Integer::sum)` in Code, wobei `m` ein `Map<String, Integer>` ist. Was macht es?
Was this page helpful?