Hashing, collisions, treeification and load factor
HashMap collisions, load factor, resize splitting, treeification thresholds, TreeNode ordering, hash-flooding history, initial capacity math and memory costs.
On this page
- Picture it first: sorting mail after a building expansion
- Collisions: unavoidable, harmless until they concentrate
- Load factor and threshold: when HashMap resizes
- Resize under the hood: lo and hi lists
- Treeification: when a bin becomes a red-black tree
- TreeNode ordering: hash, Comparable, tieBreakOrder
- Hash flooding history: why this exists
- Advanced: choosing capacity without tuning yourself into a corner
- Memory cost per entry: the hidden bill
- When collisions are your fault
- How to say it in the interview
🌳 What are HashMap collisions and treeification?
A collision happens when two different keys land in the same HashMap bucket. The map then has to search inside that bucket, first as a linked list and, in modern Java, sometimes as a red-black tree. Treeification is Java's safety net: if a bucket becomes suspiciously long, it can turn the list into a tree so lookup does not stay linear.
For this lesson, we'll focus on the senior-level internals: why collisions are unavoidable, why HashMap uses load factor 0.75, how resize doubles the table without calling hashCode() again, when a bin treeifies, how TreeNode ordering works, why this changed after hash-flooding attacks, and how to choose initial capacity. We covered the basic put and get path in How HashMap works; here we go deeper.
Below the line (out of scope)
- The beginner mental model of buckets,
hash()and(n - 1) & hash: How HashMap works - Ordered hash maps and LRU caches: LinkedHashMap and building an LRU cache
- Sorted maps and red-black trees as the main data structure: TreeMap and red-black trees
- Shared mutable maps: Hashtable, synchronizedMap and the concurrent maps
The 30-second answer
Collisions are inevitable because many keys map into a finite table, and they become painful when hashCode() is poorly distributed, for example always returning 1. HashMap resizes when size > capacity * loadFactor; the default is capacity 16, load factor 0.75, threshold 12, so the 13th insert resizes to 32. Java 8+ doubles the table and splits each old bucket into a lo list that stays at index j and a hi list that moves to j + oldCap, using (hash & oldCap) and the cached node hash, not a new hashCode() call. If a bin passes TREEIFY_THRESHOLD 8 and the table has at least MIN_TREEIFY_CAPACITY 64 buckets, it becomes a red-black tree; tree bins untreeify at 6. Tree nodes order by hash, then Comparable keys, then a deterministic tie-breaker. Start with the default load factor, presize when you know the count, and almost never tune load factor first.
What you’ll learn
- Explain why collisions are inevitable and how bad
hashCode()implementations create long bins - Use load factor
0.75andthreshold = capacity * loadFactorto predict whenHashMapresizes - Draw the Java 8+ resize split into lo and hi lists using
(hash & oldCap)without recomputing hashes - Name the treeification constants:
TREEIFY_THRESHOLD,UNTREEIFY_THRESHOLDandMIN_TREEIFY_CAPACITY - Describe
TreeNodeordering by hash,Comparable, thentieBreakOrder, and connect it to JEP 180 - Choose initial capacity from expected mappings and explain why tweaking load factor is almost never the first fix
Interview radar
Asked very often: What happens when many keys collide in HashMap?
Senior rounds probe: Why resize can split a bin with one bit, why treeify waits until 64 buckets, and why the thresholds are 8 and 6.
Production angle: A public endpoint accepts attacker-controlled keys, a bad hashCode() collapses a map into one bucket, or a large import spends seconds resizing because nobody presized it.
01Picture it first: sorting mail after a building expansion
Imagine a mailroom with 16 shelves. When the building doubles the shelf wall to 32 shelves, the clerk does not reread every envelope's full address. For each old shelf, one extra address bit decides whether the envelope stays on shelf j or moves to shelf j + 16.
That's the core trick. Doubling adds one mask bit, so every node in an old bucket has only two possible destinations.
A HashMap resize from 16 to 32 buckets splits old bucket 5 into a lo list that stays at 5 and a hi list that moves to 21 using the bit tested by hash and oldCap
Notice what does not happen. hashCode() is not called again during resize. Each node already stores the spread hash, so the map tests one bit, relinks nodes into two lists, and installs them in the new table.
02Collisions: unavoidable, harmless until they concentrate
Collisions are not a bug by themselves. A 32-bit hash has about 4.29 billion possible values, and the table usually has far fewer buckets than that. The pigeonhole principle says different keys must eventually share a hash value or a bucket.
The real question is distribution. If hashes spread evenly, each bucket stays short and lookup is near O(1). If many keys share low bits, or worse, share the same hash, one bucket gets a long line.
Try it yourself first
Before reading the output, decide how many buckets this map will really use.
final class BadKey {
private final int id;
BadKey(int id) { this.id = id; }
@Override public boolean equals(Object o) {
return o instanceof BadKey other && id == other.id;
}
@Override public int hashCode() {
return 1; // every key collides
}
}
Map<BadKey, String> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
map.put(new BadKey(i), "order-" + i);
}
System.out.println(map.size()); // 10
System.out.println(map.get(new BadKey(7))); // order-7
The map still works because equals separates the keys inside the bucket. But the performance shape is terrible: all ten entries start in one bin. Below 64 buckets, Java prefers to resize; at 64 or more, a bin that grows past 8 nodes can treeify.
String is the common good example. Its hash is specified as:
int h = 0;
for (int i = 0; i < s.length(); i++) {
h = 31 * h + s.charAt(i);
}
The multiplier 31 is a small odd prime, and the formula is stable across JVMs because String.hashCode() is part of the API contract. Stable does not mean collision-free. Classic pairs such as "Aa" and "BB" collide, which is useful for tests, but normal English-like strings distribute well enough for everyday maps.
Many candidates say "a collision means the new value overwrites the old one". It only overwrites when the keys are equal. Same bucket or same hash is not enough; HashMap compares stored hash, then ==, then equals before it replaces a value.
03Load factor and threshold: when HashMap resizes
Load factor is the fullness limit of the table. The default 0.75 means resize after the map holds more than 75% as many entries as buckets. With capacity 16, threshold is 16 * 0.75 = 12; the 13th new mapping triggers a resize.
Why 0.75? It is a pragmatic trade-off. Lower values use more memory and fewer collisions. Higher values save table slots but make bins longer and resizes later. Java's default is a good balance for general-purpose server code, and I almost never change it.
| Capacity | Load factor | Threshold | Insert that resizes |
|---|---|---|---|
| 16 | 0.75 | 12 | 13th |
| 32 | 0.75 | 24 | 25th |
| 64 | 0.75 | 48 | 49th |
| 1,024 | 0.75 | 768 | 769th |
Try it yourself first
You expect 1,000 entries. What initial capacity avoids resize at the default load factor?
int expected = 1_000;
int needed = (int) (expected / 0.75f) + 1; // 1334
Map<String, Integer> oldStyle = new HashMap<>(needed); // rounded up to 2048 buckets
Map<String, Integer> newStyle = HashMap.newHashMap(expected); // Java 19+
The capacity must be a power of two, so 1334 rounds up to 2048. That gives threshold 2048 * 0.75 = 1536, enough room for 1,000 mappings. Passing new HashMap<>(1_000) is the common mistake: 1,000 rounds to 1,024, threshold 768, and the map resizes before you reach 1,000 entries.
Presizing is most useful when the expected count is large and known. Going from 16 buckets to 2,097,152 buckets takes 17 doublings. For 1,000,000 mappings, expected / 0.75 + 1 is 1,333,334, rounded up to 2,097,152 buckets; the table alone is about 8 MB with 4-byte compressed references, before any nodes, keys or values.
04Resize under the hood: lo and hi lists
Resize is where a lot of memorised answers fall apart. Older explanations say HashMap rehashes everything. Java 8+ does not call every key's hashCode() again. It reuses the cached spread hash in each node.
Here is the reason. Old capacity is a power of two, say 16. Old mask is 0b01111; new capacity is 32 and new mask is 0b11111. The new mask adds exactly one bit, the bit whose value is oldCap.
int oldCap = 16;
int oldIndex = hash & (oldCap - 1);
int newIndex = hash & ((oldCap * 2) - 1);
// During resize:
// if ((hash & oldCap) == 0) newIndex == oldIndex
// else newIndex == oldIndex + oldCap
So a bucket at index 5 can only split to index 5 or index 21. The JDK walks the old bin once and builds two chains: loHead/loTail for nodes where (hash & oldCap) == 0, and hiHead/hiTail for nodes where it is non-zero. Then it writes the two heads into the new table.
This matters for performance and correctness. Performance improves because no user code runs during resize. Correctness improves because Java 8 also preserves relative order inside each half and appends nodes, avoiding the old Java 7 head-insertion resize pattern that could form cyclic lists under unsynchronized concurrent mutation.
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- Treeification: when a bin becomes a red-black tree
- TreeNode ordering: hash, Comparable, tieBreakOrder
- Hash flooding history: why this exists
- Advanced: choosing capacity without tuning yourself into a corner
- Memory cost per entry: the hidden bill
- When collisions are your fault
- How to say it in the interview
From ₹199 · one-time payment · No auto-renewal
Further reading (8)
- HashMap (Java SE 21 API)
- OpenJDK HashMap source, JDK 21
- JEP 180: Handle Frequent HashMap Collisions with Balanced Trees
- String.hashCode (Java SE 21 API)
- Crosby and Wallach: Denial of Service via Algorithmic Complexity Attacks
- Baeldung: The Java HashMap Under the Hood
- Baeldung: Java HashMap Load Factor
- JOL: Java Object Layout