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).
| Methode | Was 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:
- Berechne
h = hashCode(key). Mische die oberen und unteren 16 Bits —h ^ (h >>> 16)— damit ein Hash wie0x12340000seine oberen Bits beim Maskieren nicht verliert. - Maske:
i = h & (table.length - 1). Das isth mod lengthfür Potenz-von-zwei-Längen und schneller als der Modulo-Operator. - Durchlaufe die Kette bei
table[i]. Falls ein Knoten mit einem gleichen Schlüssel existiert, überschreibe seinen Wert und gib den alten zurück. - Andernfalls wird ein neuer Knoten vorangestellt (oder seit Java 8 angehängt).
- 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 doublingsOder 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 trueDie 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")); // hitIterationsreihenfolge — 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.
Was aus dem Lauf mitzunehmen ist:
mergereduziert 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
putdas Ergebnis. Beachte, dass esget+putstattcomputeIfAbsentverwendet — ein rekursivescomputeIfAbsentmutiert die Map, während seine eigene Mapping-Funktion noch läuft, und seit Java 9 wirft das eineConcurrentModificationException. ReservierecomputeIfAbsentfür nicht-rekursive "Lade-oder-Berechne"-Suchen. - Die Null-Mehrdeutigkeit ist real.
getgab auf die gleiche Weisenullfür einen vorhandenen und einen fehlenden Schlüssel zurück. Der einzige Weg, sie zu unterscheiden, istcontainsKey— 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 korrektesequals/hashCodekostenlos. 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.