Why this matters
- This is the most asked internals question in Java interviews, and the answer touches hashing, collisions, tree conversion and resizing in one coherent story.
- The failure modes are real: a poor
hashCodeturns O(1) lookups into O(n) scans, and a mutable key loses data. - Understanding resize cost is what makes pre-sizing a map feel obvious rather than pedantic.
The structure
Bucket index comes from the key's hash, not its insertion order — which is why HashMap iteration has no predictable sequence.
- The map holds a
Node[] table, always a power-of-two length. - Each
Nodestores the hash, the key, the value and a next pointer. - Nodes landing on the same index form a chain.
- A chain that grows past a threshold is converted into a red-black tree.
Finding the bucket
Three steps turn a key into an array index.
// 1. The key's own hash code
int raw = key.hashCode();
// 2. Spreading: mix the high bits down into the low bits
int hash = raw ^ (raw >>> 16);
// 3. Index: a mask, equivalent to hash % length but far cheaper
int index = hash & (table.length - 1);
Step 3 is the reason the table length is a power of two. For length 16, length - 1 is 0b1111, so the
mask keeps the lowest four bits — exactly a modulo, computed as a single bitwise AND.
Step 2 exists because of step 3. Masking only looks at the low bits, so two keys differing only in their high bits would collide every time. XOR-ing the top 16 bits down mixes that information into the part that is actually used.
put, step by step
map.put("apple", 10);
- Compute the spread hash and mask it to an index.
- If the bucket is empty, store a new node there. Done.
- If the bucket is occupied, walk the chain. For each node, compare the stored hash first, then compare keys with
==orequals. - If a node matches, replace its value and return the old one.
- If no node matches, append a new node at the end of the chain.
- If the chain has reached 8 nodes and the table is at least 64 long, treeify the bucket.
- If
sizenow exceeds capacity × load factor, resize.
The hash comparison before equals is a deliberate optimisation: comparing two int values is far
cheaper than calling equals, and unequal hashes guarantee unequal keys.
get, step by step
map.get("apple");
Compute the index the same way, then walk the chain comparing hash and key. Found means return the value;
exhausting the chain means return null. For a treeified bucket the walk becomes a binary search.
With a good hash the chain holds one node, which is where O(1) comes from. With every key colliding the chain holds all n, which is the O(n) worst case.
Treeification
Java 8 added a defence against long chains.
| Aspect | Chain state | Behaviour |
|---|---|---|
| 1 to 7 nodes | Linked list | Linear scan — fine at this length |
| 8 or more nodes, table ≥ 64 | Converted to a red-black tree | O(log n) search within the bucket |
| 8 or more nodes, table < 64 | Resize instead of treeify | Spreading out is cheaper than a tree |
| Shrinks back to 6 nodes | Reverts to a linked list | Hysteresis prevents flip-flopping |
1 to 7 nodes
Chain stateLinked listBehaviourLinear scan — fine at this length8 or more nodes, table ≥ 64
Chain stateConverted to a red-black treeBehaviourO(log n) search within the bucket8 or more nodes, table < 64
Chain stateResize instead of treeifyBehaviourSpreading out is cheaper than a treeShrinks back to 6 nodes
Chain stateReverts to a linked listBehaviourHysteresis prevents flip-flopping
The gap between 8 and 6 is deliberate: converting back and forth at a single threshold would thrash.
Resizing
When size exceeds capacity × 0.75, the table doubles and every entry is redistributed.
Sizing a map up front with new HashMap<>(expected / 0.75f + 1) avoids repeated rehashing when you already know the volume.
Doubling the length adds exactly one bit to the mask, which gives a neat property: an entry either stays
at its current index or moves to index + oldCapacity. Nothing else is possible, so Java 8 splits each
chain into a "stay" list and a "move" list in one pass, preserving relative order.
// 16 → 12 entries triggers a resize to 32
// 32 → 24 entries triggers a resize to 64
// Each resize is O(n) over the current contents
Map<String, Integer> sized = new HashMap<>(1334); // 1000 entries, never resizes
Map<String, Integer> modern = HashMap.newHashMap(1000); // Java 19+, same effect
Why the hashCode contract matters here
class BadKey {
final String value;
BadKey(String value) { this.value = value; }
@Override public boolean equals(Object o) {
return o instanceof BadKey k && value.equals(k.value);
}
@Override public int hashCode() {
return 1; // legal, and catastrophic
}
}
Every key hashes to 1, so every entry lands in one bucket. The map degenerates into a single chain — O(n) for every operation, with the memory overhead of a hash map and none of the speed. Treeification limits the damage to O(log n), but the structure is still doing far more work than a plain list would.
The other direction is equally broken:
// equals says these are equal, hashCode says they belong in different buckets
// → put stores two entries for the "same" key, get finds neither reliably
What a map needs from a key
- A well-distributed
hashCodeso buckets fill evenly. equalsandhashCodeusing the same fields, so the bucket and the comparison agree.- Immutability, so the hash computed at insertion stays valid.
- Ideally
Comparable, so treeified buckets can order properly.
null handling and the family
HashMap<String, String> map = new HashMap<>();
map.put(null, "allowed"); // exactly one null key, stored in bucket 0
map.put("key", null); // any number of null values
A null key cannot be hashed, so it is special-cased to index 0.
| Aspect | Implementation | Difference from HashMap |
|---|---|---|
| LinkedHashMap | Adds a doubly linked list across entries | Predictable iteration order; basis of an LRU cache |
| TreeMap | Red-black tree, not a hash table | O(log n), sorted, no nulls, navigable |
| ConcurrentHashMap | Per-bucket locking, lock-free reads | Thread-safe, forbids null keys and values |
| Hashtable | Synchronizes every method | Legacy; slower and not more useful |
| IdentityHashMap | Uses == instead of equals | For identity-based caches only |
| WeakHashMap | Keys are weak references | Entries vanish when the key is collected |
LinkedHashMap
ImplementationAdds a doubly linked list across entriesDifference from HashMapPredictable iteration order; basis of an LRU cacheTreeMap
ImplementationRed-black tree, not a hash tableDifference from HashMapO(log n), sorted, no nulls, navigableConcurrentHashMap
ImplementationPer-bucket locking, lock-free readsDifference from HashMapThread-safe, forbids null keys and valuesHashtable
ImplementationSynchronizes every methodDifference from HashMapLegacy; slower and not more usefulIdentityHashMap
ImplementationUses == instead of equalsDifference from HashMapFor identity-based caches onlyWeakHashMap
ImplementationKeys are weak referencesDifference from HashMapEntries vanish when the key is collected
ConcurrentHashMap forbids nulls so a null return unambiguously means absent, which matters when you cannot lock.
Common misreadings
- "
HashMapis O(1), full stop." It is O(1) average with a good hash. The worst case is O(log n) since Java 8, and was O(n) before. - "The table length can be any number." It is always a power of two, which is what makes the masking trick work.
- "Treeification happens at 8 entries in the map." At 8 nodes in a single bucket, and only once the table is at least 64 long.
- "Resizing rehashes every key." The stored hash is reused; only the index is recomputed, and the split is a single pass.
- "Java 8 made
HashMapthread-safe." It removed one specific livelock. Concurrent writes are still unsafe. - "
hashCodemust be unique." It must be well distributed. Collisions are expected and handled. - "A null key throws."
HashMappermits one;ConcurrentHashMapandTreeMapdo not.
Quick recall
Everything you need if you only revisit this box.
- Structure: a power-of-two
Node[], each node holding hash, key, value and next. - Index =
(hash ^ (hash >>> 16)) & (length - 1). Spreading mixes high bits down; the mask replaces modulo. putwalks the chain comparing hash first, thenequals, replacing on a match and appending otherwise.- A bucket treeifies at 8 nodes when the table is ≥ 64, and reverts at 6. Worst case went from O(n) to O(log n) in Java 8.
- Resize at size > capacity × 0.75: double the table; each entry stays or moves to
index + oldCapacity. - A constant
hashCodedegenerates the map into one chain. Use the same fields inequalsandhashCode, and immutable keys. HashMapallows one null key and many null values;ConcurrentHashMapandTreeMapallow none.HashMapis not thread-safe — useConcurrentHashMap.
Test yourself
Answer these before moving on — recall is what makes it stick.