Java HashMap ist eine der häufigsten Datenstrukturen, die in der Java-Programmierung verwendet werden, um Schlüssel-Wert-Paare effizient zu speichern und abzurufen. Um die Interna von Java HashMaps zu verstehen, müssen wir einige grundlegende Begriffe wie Hashing, Buckets, Capacity, Load Factors und Collisions verstehen. Darüber hinaus werden wir den Vorgang des Rehashings besprechen und wie man es in Fällen großer HashMaps durch passende Wahl der Initial Capacity vermeiden kann.
Hashing und die Bedeutung von Hash Codes
Das Herzstück einer HashMap ist die Hash-Funktion. Eine Hash-Funktion ist eine Methode, die einen Eingabewert, in diesem Fall den Schlüssel, in einen numerischen Wert, den Hash-Code, umwandelt. Dieser Hash-Code dient als Index, um den entsprechenden Wert in der HashMap schnell abzurufen. Die Qualität der Hash-Funktion ist entscheidend, um die Effizienz der HashMap sicherzustellen. Idealerweise sollte die Hash-Funktion sicherstellen, dass Schlüssel gleichmäßig über die verfügbaren Speicherbereiche verteilt werden.
Buckets und Capacity
In einer HashMap werden Schlüssel-Wert-Paare in sogenannten „Buckets“ gespeichert. Buckets sind in der Regel Arrays oder Listen, die dazu dienen, Schlüssel-Wert-Paare zu gruppieren, die denselben Hash-Code haben. Die HashMap verfügt über eine bestimmte Anzahl von Buckets, die als Capacity bezeichnet wird. Die Kapazität ist zu Beginn der Erstellung einer HashMap festgelegt und bestimmt die maximale Anzahl von Buckets, die die HashMap halten kann.
Load Factors und die Auswirkungen auf die Leistung
Der Load Factor ist ein Wert zwischen 0 und 1, der angibt, wie voll die HashMap ist. Er wird berechnet, indem die Anzahl der in der HashMap gespeicherten Schlüssel-Wert-Paare durch die Kapazität der HashMap geteilt wird. Wenn der Load Factor hoch ist, bedeutet dies, dass die HashMap fast voll ist, und wenn er niedrig ist, gibt es noch viel Platz in der HashMap.
Ein hoher Load Factor führt zu einer geringeren Leistung der HashMap, da die Wahrscheinlichkeit von Kollisionen (dazu später mehr) steigt. Um dies zu vermeiden, ist es wichtig, den Load Factor im Auge zu behalten und gegebenenfalls die Kapazität der HashMap anzupassen.
Collisions: Das Problem der Hash-Kollisionen
Eine Hash-Kollision tritt auf, wenn zwei verschiedene Schlüssel denselben Hash-Code erzeugen. Dies kann vorkommen, da die meisten Hash-Funktionen eine begrenzte Anzahl von möglichen Hash-Codes haben, während es unendlich viele mögliche Schlüssel gibt. Wenn eine Kollision auftritt, muss die HashMap eine Strategie zur Bewältigung dieser Situation haben.
Rehashing: Die Lösung für Kollisionen
Rehashing ist der Prozess, bei dem die HashMap ihre Kapazität erhöht und alle Schlüssel-Wert-Paare in die neuen Buckets umverteilt, um Kollisionen zu vermeiden. Dies ist notwendig, wenn der Load Factor zu hoch wird und die HashMap ineffizient wird.
Rehashing erfolgt normalerweise automatisch in der HashMap, wenn der Load Factor einen bestimmten Schwellenwert überschreitet. Während des Rehashing wird die Kapazität der HashMap erhöht, und alle Schlüssel-Wert-Paare werden neu gehasht und in die entsprechenden Buckets verschoben.
Wann passiert Rehashing und seine Auswirkungen
Rehashing tritt auf, wenn der aktuelle Load Factor den vordefinierten Schwellenwert überschreitet. In Java 8 beträgt dieser Schwellenwert in der HashMap etwa 0,75 (75%). Das bedeutet, dass, wenn die Anzahl der gespeicherten Schlüssel-Wert-Paare 75% der Kapazität der HashMap erreicht, ein Rehashing ausgelöst wird.
Die Auswirkungen von Rehashing sind in der Regel:
- Leistungseinbußen: Während des Rehashing-Prozesses wird die gesamte HashMap neu organisiert, was zu einer vorübergehenden Verschlechterung der Leistung führen kann. Dieser Prozess kann einige Zeit in Anspruch nehmen, insbesondere bei großen HashMaps.
- Speicherplatzverbrauch: Nach dem Rehashing wird die Kapazität der HashMap erhöht, was zu einem höheren Speicherbedarf führt. Es ist wichtig sicherzustellen, dass genügend Speicherplatz verfügbar ist, um das Rehashing durchzuführen.
- Bessere Leistung nach Rehashing: Nachdem das Rehashing abgeschlossen ist, sollten Kollisionen reduziert und die HashMap wieder effizienter sein.
Vermeidung von unnötigem Rehashing durch richtige Wahl der Initial Capacity
Um unnötiges Rehashing zu vermeiden, ist es möglich, die richtige Anfangskapazität (Initial Capacity) für die HashMap festzulegen. Eine zu niedrige Anfangskapazität kann dazu führen, dass die HashMap häufiger neu organisiert werden muss, was die Leistung beeinträchtigt. Eine zu hohe Anfangskapazität kann hingegen zu einem unnötig hohen Speicherverbrauch führen.
Der Standardwert für die Capacity ist 16, und ausreichend für die allermeisten HashMaps. Die initialen Rehashings sind auch noch verhältnismäßig günstig, da wenige Werte in der Map liegen. Deswegen fährt man als Entwickler mit dem Vorgehen, für die meisten HashMaps keine eigene Anfangskapazität festzulegen, in der Regel gut. Aber: bei erwartbar großen Maps kann dies anders aussehen! Rehashing von zehntausenden von Werten ist wesentlich teurer als bei ein paar Dutzend Elementen und kann zu spürbarer Verzögerung in der Reaktivität einer Anwendung führen. Es ist daher bei erwartbar großen Maps ratsam, die Anfangskapazität manuell basierend auf der erwarteten Größe der HashMap und dem Load Factor zu wählen. Eine grobe Faustregel ist, die Anfangskapazität so zu wählen, dass die HashMap etwa doppelt so viele Buckets hat, wie Sie erwarten, Schlüssel-Wert-Paare zu speichern. Dies hilft, unnötiges Rehashing zu vermeiden und die HashMap effizient zu halten.
int initialCapacity = 50000; // Beispielsweise
float loadFactor = 0.75f; // Standard in Java HashMap
HashMap<KeyType, ValueType> bigMap = new HashMap<>(initialCapacity, loadFactor);
Code-Sprache: JavaScript (javascript)
In diesem Beispiel wird eine große HashMap mit einer Anfangskapazität von 50.000 und einem Load Factor von 0,75 erstellt. Dies sollte für eine große Map, in der voraussichtlich bis zu etwa 30.000 Key-Value-Paare gespeichert werden sollen, ausreichen.