PrepZone Logo
PrepZone

How HashMap Works Inside

Buckets, collision chains, treeification and resizing — the most asked internals question there is.

Read these first

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 hashCode turns 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

table — array of buckets
bucket 0empty
bucket 1"asha" → 30
bucket 5 — collision chain
"ravi" → 24next →
"neha" → 29next → null
bucket 9 — treeified
Red-black treeMore than 8 entries: lookup becomes O(log n)

Bucket index comes from the key's hash, not its insertion order — which is why HashMap iteration has no predictable sequence.

The hash picks a bucket. Keys that land in the same bucket form a short list, and once a bucket grows past eight entries it becomes a balanced tree so lookups stay fast.
  • The map holds a Node[] table, always a power-of-two length.
  • Each Node stores 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.

Java
// 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

Java
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 == or equals.
  • 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 size now 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

Java
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.

AspectChain stateBehaviour
1 to 7 nodesLinked listLinear scan — fine at this length
8 or more nodes, table ≥ 64Converted to a red-black treeO(log n) search within the bucket
8 or more nodes, table < 64Resize instead of treeifySpreading out is cheaper than a tree
Shrinks back to 6 nodesReverts to a linked listHysteresis prevents flip-flopping
  • 1 to 7 nodes

    Chain stateLinked list
    BehaviourLinear scan — fine at this length
  • 8 or more nodes, table ≥ 64

    Chain stateConverted to a red-black tree
    BehaviourO(log n) search within the bucket
  • 8 or more nodes, table < 64

    Chain stateResize instead of treeify
    BehaviourSpreading out is cheaper than a tree
  • Shrinks back to 6 nodes

    Chain stateReverts to a linked list
    BehaviourHysteresis 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.

16 buckets12 entries — fine
Threshold hit16 × 0.75 = 12
32 bucketsEntries rehashed

Sizing a map up front with new HashMap<>(expected / 0.75f + 1) avoids repeated rehashing when you already know the volume.

With the default load factor of 0.75 a 16-bucket table resizes at the 13th entry. Doubling keeps chains short, which is what preserves constant-time lookups.

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.

Java
// 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

Java
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:

Java
// 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 hashCode so buckets fill evenly.
  • equals and hashCode using 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

Java
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.

AspectImplementationDifference from HashMap
LinkedHashMapAdds a doubly linked list across entriesPredictable iteration order; basis of an LRU cache
TreeMapRed-black tree, not a hash tableO(log n), sorted, no nulls, navigable
ConcurrentHashMapPer-bucket locking, lock-free readsThread-safe, forbids null keys and values
HashtableSynchronizes every methodLegacy; slower and not more useful
IdentityHashMapUses == instead of equalsFor identity-based caches only
WeakHashMapKeys are weak referencesEntries vanish when the key is collected
  • LinkedHashMap

    ImplementationAdds a doubly linked list across entries
    Difference from HashMapPredictable iteration order; basis of an LRU cache
  • TreeMap

    ImplementationRed-black tree, not a hash table
    Difference from HashMapO(log n), sorted, no nulls, navigable
  • ConcurrentHashMap

    ImplementationPer-bucket locking, lock-free reads
    Difference from HashMapThread-safe, forbids null keys and values
  • Hashtable

    ImplementationSynchronizes every method
    Difference from HashMapLegacy; slower and not more useful
  • IdentityHashMap

    ImplementationUses == instead of equals
    Difference from HashMapFor identity-based caches only
  • WeakHashMap

    ImplementationKeys are weak references
    Difference 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

  • "HashMap is 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 HashMap thread-safe." It removed one specific livelock. Concurrent writes are still unsafe.
  • "hashCode must be unique." It must be well distributed. Collisions are expected and handled.
  • "A null key throws." HashMap permits one; ConcurrentHashMap and TreeMap do 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.
  • put walks the chain comparing hash first, then equals, 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 hashCode degenerates the map into one chain. Use the same fields in equals and hashCode, and immutable keys.
  • HashMap allows one null key and many null values; ConcurrentHashMap and TreeMap allow none.
  • HashMap is not thread-safe — use ConcurrentHashMap.

Test yourself

Answer these before moving on — recall is what makes it stick.