66. HashMap Internals
HashMap stores entries in an array of buckets; collision entries are stored in a linked list that treeifies to a red-black tree at 8+ entries (Java 8+); a good hashCode distribution minimises collisions.
Explanation
HashMap internally has an Entry<K,V>[] array (the "table"). For put(key, value): compute hash(key), find index = hash & (capacity - 1), and place the entry in table[index]. Collisions (multiple keys at the same index) form a linked list at that bucket. Since Java 8, if a bucket's linked list grows to 8+ entries and the table has at least 64 slots, the list is treeified into a red-black tree โ turning O(n) worst-case lookups back to O(log n).
Resizing: when the number of entries exceeds capacity * loadFactor (default 0.75), the table doubles and all entries are rehashed. Resizing is expensive โ pre-size with new HashMap<>(expectedSize / 0.75 + 1) if you know the count.
hashCode matters: if all keys hash to the same bucket, HashMap degrades to O(n) (or O(log n) with treeification). Classes used as keys must have well-distributed hashCode implementations. Mutable keys are dangerous โ if the key's hash changes after it's in the map, you'll never find the entry again.
Example
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class HashMapInternals {
// Object with intentionally bad hashCode: all keys collide
static class BadKey {
final int id;
BadKey(int id) { this.id = id; }
@Override public int hashCode() { return 1; } // all same bucket!
@Override public boolean equals(Object o) {
return o instanceof BadKey b && id == b.id;
}
@Override public String toString() { return "K" + id; }
}
// Well-distributed hashCode
static class GoodKey {
final int id;
GoodKey(int id) { this.id = id; }
@Override public int hashCode() { return Objects.hash(id); }
@Override public boolean equals(Object o) {
return o instanceof GoodKey g && id == g.id;
}
}
public static void main(String[] args) {
// Demonstrate performance impact of hash distribution
int N = 1000;
Map<BadKey, Integer> bad = new HashMap<>(N * 2);
Map<GoodKey, Integer> good = new HashMap<>(N * 2);
for (int i = 0; i < N; i++) {
bad.put(new BadKey(i), i);
good.put(new GoodKey(i), i);
}
long t1 = System.nanoTime();
for (int i = 0; i < N; i++) bad.get(new BadKey(i));
long t2 = System.nanoTime();
for (int i = 0; i < N; i++) good.get(new GoodKey(i));
long t3 = System.nanoTime();
System.out.printf("Bad hashCode lookup: %,d ns%n", t2 - t1);
System.out.printf("Good hashCode lookup: %,d ns%n", t3 - t2);
System.out.println("(Bad is much slower due to all keys in one bucket)");
// Mutable key trap
var mutableKey = new java.awt.Point(1, 2); // hashCode depends on x,y
Map<java.awt.Point, String> trap = new HashMap<>();
trap.put(mutableKey, "value");
System.out.println("Before: " + trap.get(mutableKey)); // "value"
mutableKey.x = 99; // mutate the key โ changes its hash!
System.out.println("After: " + trap.get(mutableKey)); // null (wrong bucket now)
}
}Sources
- https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/HashMap.html โ HashMap API
- https://openjdk.org/jeps/180 โ JEP 180: TreeBins in HashMap (Java 8)