Hash tables in Java
Hashing, collisions, load factor — the theory behind HashMap.
From key to bucket
A hash table turns a key into an array index. HashMap calls hashCode(), mixes the high bits into the low ones, and maps the result onto its bucket array. Like a coat check: your ticket number says which hook to look at. If keys spread well, get and put are O(1) on average.
int h = key.hashCode();
h = h ^ (h >>> 16); // mix high bits
int bucket = (table.length - 1) & h;Collisions and tree bins
Different keys can land in the same bucket: a collision. HashMap keeps them in a chain and checks each with equals. When a chain reaches 8 entries and the table has at least 64 buckets, it converts that bin into a red-black tree. In a smaller table it resizes instead: long chains there usually just mean too few buckets.
Load factor and resizing
A default HashMap has 16 buckets and a load factor of 0.75. The threshold is 16 x 0.75 = 12, so adding the 13th entry triggers a resize: the table doubles to 32 and entries are redistributed, keeping chains short.
Equal keys, one entry
Records generate equals and hashCode for you. What does this print?
record Pt(int x, int y) {}
void main() {
var m = new HashMap<Pt, String>();
m.put(new Pt(1, 2), "A");
m.put(new Pt(1, 2), "B");
IO.println(m.size());
IO.println(m.get(new Pt(1, 2)));
}1 B2 A2 B
Show the answer
The two Pt(1, 2) objects are equal and have the same hash, so the second put lands in the same bucket and replaces the value. The contract: if a.equals(b), then a.hashCode() == b.hashCode(). Break it and the map searches the wrong bucket.
Never mutate a key
The entry was filed in the bucket for the hash of [1]. After add(2), key hashes differently, so get(key) looks in another bucket: null. List.of(1) finds the right bucket, but equals fails against the stored [1, 2]: null again. The entry is stranded.
var key = new ArrayList<>(List.of(1));
var m = new HashMap<List<Integer>, String>();
m.put(key, "found");
key.add(2);
m.get(key); // null
m.get(List.of(1)); // nullEvery hash is 42
A buggy key class returns 42 from hashCode() for every object. What happens to HashMap lookups?
Think about it, then reveal the answer
All keys collide into one bucket, so lookups degrade toward O(n). Once that bin is treeified, lookups improve to O(log n), but only if the keys are Comparable. HashMap doesn't switch hash functions or throw; hashing only helps when keys spread out.
On the job
Mutable keys and broken hashCode overrides cause "the entry is in the map but get returns null" bugs. Use immutable keys like records and strings, always override equals and hashCode together, and presize maps you know will be large to avoid repeated resizing.
Key takeaways
- Average O(1) get/put if hashCode spreads keys well
- Long bins treeify at 8 entries when capacity is at least 64
- Default 16 buckets × 0.75 → resize on the 13th entry
- Equal objects must have equal hash codes
💡 A coat check: your ticket number says which hook to look at, and sometimes two coats share a hook.
Tree bins were added to HashMap in Java 8 (JEP 180) to keep lookups fast even when many keys collide, including deliberately crafted collision attacks.
Practice questions
What does this print?
var key = new ArrayList<>(List.of(1));
var m = new HashMap<List<Integer>, String>();
m.put(key, "found");
key.add(2);
System.out.println(m.get(key));
System.out.println(m.get(List.of(1)));- null null
- found null
- found found
- null found
Check your answer
null null. The entry was filed under the hash of [1]. After mutation, key hashes differently and lands in another bucket; List.of(1) finds the right bucket, but equals fails against [1, 2]. Never mutate a key that's inside a map.
A HashMap created with default settings has 16 buckets. When does it first resize?
- When the 13th entry is added (size > 16 × 0.75)
- When the 16th entry is added
- When any bucket holds 8 entries
- Never; chains just grow longer
Check your answer
When the 13th entry is added (size > 16 × 0.75). The threshold is capacity × load factor = 12. Going past it doubles the table to 32 buckets and redistributes entries, keeping chains short.