JavaCollectionsintermediate
Updated:

How Does HashMap Work Internally? (Java Interview Deep Dive)

6 min read

The single most-asked Java internals question, answered in full — hashing, buckets, collisions, treeification, load factor and resize — the way interviewers want to hear it.

TL;DR – Quick Answer

Internally a HashMap is an array of buckets. put computes the key's hashCode, spreads its bits, and maps it to a bucket with (n-1) & hash; collisions form a linked list that converts to a red-black tree past 8 nodes (with at least 64 buckets). When size exceeds capacity times the 0.75 load factor, the table doubles and entries are redistributed. Interviewers grade whether you can narrate put, get and resize precisely.

On This Page

Why this is the most-asked Java internals question

"How does HashMap work internally?" appears in nearly every Java interview because it has verifiable depth: it can be answered in one sentence or in five layers, and the layer where you stop tells the interviewer your level precisely. It also touches hashing, equality, memory and even concurrency, so a single question lets them probe several fundamentals at once.

This page gives you the full narration — put, get, collisions, treeification, load factor and resize — in the order interviewers expect. Practice it until you can whiteboard it without pausing.

How to narrate it in an interview

Answer from the data structure outward: "a HashMap is an array of buckets; each bucket holds entries whose keys hash to that index." Then trace an operation step by step rather than listing facts. The narration — hash, spread, index, walk the bucket, compare hash-then-equals — is what earns the marks, because it proves you understand the mechanism, not just the vocabulary.

Q1. What is the internal structure of a HashMap?

A HashMap holds a Node[] array — the bucket table. Each Node stores the key, value, the key's cached hash, and a next pointer forming a linked list for that bucket. When a bucket grows large it becomes a red-black tree of TreeNodes instead of a list.

The cached hash inside each node is an easy detail to miss and a good one to mention: hashes are computed once at insertion and stored, so resize does not recompute them. The array is the map; buckets are its slots.

Interview note: Follow-up: "what is the initial capacity?" 16 buckets by default, with a 0.75 load factor, so the first resize happens at 12 entries.

Q2. Walk through what happens on put(key, value).

HashMap calls key.hashCode(), spreads the bits, computes the bucket index with (n-1) & hash, and goes to that bucket. Empty bucket: insert a new node. Non-empty: walk the chain comparing hash then equals — replace the value on a match, append a new node otherwise. After insertion it checks whether to treeify the bucket or resize the table.

// conceptual sketch of the index computation
int h = key.hashCode();
int spread = h ^ (h >>> 16);     // mix high bits into low bits
int index = (n - 1) & spread;    // bucket, n is a power of two

The bit-spread step (h ^ (h >>> 16)) exists because (n-1) & hash only uses the low bits; mixing the high bits in reduces collisions when hashCodes differ mainly in their upper bits.

Interview note: Trap: "does put replace on equal keys or add a duplicate?" It replaces the value when the key equals an existing one and returns the old value; keys never duplicate.

Q3. Why must equal keys have equal hashCodes?

Because get finds the bucket via hashCode() first and only searches that bucket. If two equal objects produce different hashes, an entry stored via one instance is looked up in a different bucket via the other — and the lookup fails even though the keys are "equal."

class Point {
    int x, y;
    // equals overridden but hashCode NOT
}
Map<Point, String> m = new HashMap<>();
m.put(new Point(1, 2), "home");
m.get(new Point(1, 2));   // null — different default hashCode, wrong bucket

This is the single most common self-inflicted HashMap bug, and the reason keys should be immutable — if a key's fields change after insertion, its hash no longer matches its bucket and the entry is effectively lost while still consuming memory.

Interview note: Follow-up: "the mutated key's entry is stuck — how do you retrieve it?" Iterate entrySet(); only hashed lookup is broken, so a full scan still finds it.

Q4. How are collisions handled, and what is treeification?

Colliding keys share a bucket as a linked list. When a single bucket's chain exceeds 8 nodes AND the table has at least 64 buckets, that bucket converts to a red-black tree, turning worst-case lookup in the bucket from O(n) to O(log n). It reverts to a list when it shrinks below 6 nodes.

The two thresholds — 8 to treeify, 6 to untreeify — leave a gap so a bucket hovering at the boundary does not flip-flop on every add and remove. The 64-bucket precondition matters: below it, HashMap resizes instead of treeifying, because a tiny table with a long chain is better fixed by spreading entries.

Interview note: Trap: "chain hits 8 in a 16-bucket table — does it treeify?" No — the table is under 64 buckets, so it resizes first; treeification needs both conditions.

Q5. What do load factor and initial capacity control?

Capacity is the bucket count; load factor (default 0.75) is the fill ratio that triggers resize. When size > capacity × loadFactor, the table doubles and every entry is redistributed. Lower load factor wastes memory on empty buckets; higher piles up collisions.

The practical detail: new HashMap<>(16) resizes at 12 entries, not 16. To hold N entries without a resize, size for N / 0.75:

Map<String, User> byId = new HashMap<>(
    (int) Math.ceil(10_000 / 0.75));   // holds ~10k without resizing

Pre-sizing matters because each resize is an O(n) redistribution — avoiding a dozen of them on a known-large map is a real optimization.

Interview note: Follow-up: "does resize recompute hashCodes?" No — hashes are cached in the nodes; only the index mapping changes because n changed.

Q6. Explain what happens during a resize.

On resize the table doubles, preserving the power-of-two invariant. In Java 8+, each entry moves to either its old index or old index plus old capacity — decided by a single bit of the hash — so entries split cleanly between the "low" and "high" halves without recomputing hashes.

This one-bit split is why doubling keeps rehashing cheap and why capacity must stay a power of two. It also fixed the Java 7 flaw where concurrent resize could invert a bucket's list and create an infinite loop.

Interview note: Trap: "which bit decides the new index?" The bit at the position equal to the old capacity: if (hash & oldCap) == 0 the entry stays; otherwise it moves to index + oldCap.

Q7. Why is capacity always a power of two?

So the index computation (n-1) & hash works as a fast modulo. When n is a power of two, n-1 is all ones in binary, and the AND keeps exactly the low bits of the hash — equivalent to hash % n but far cheaper. If n weren't a power of two, the AND would skip buckets and distribute unevenly.

If you pass a non-power-of-two initial capacity, HashMap rounds it up to the next power of two internally. This invariant is what makes both indexing and resize efficient, so it ties Q2, Q5 and Q6 together.

Interview note: Follow-up: "you pass capacity 17 — what happens?" It becomes 32, the next power of two; HashMap never uses a non-power-of-two table size.

Q8. How does HashMap differ from ConcurrentHashMap under threads?

HashMap is not thread-safe: concurrent writes can lose updates or corrupt the structure. ConcurrentHashMap allows concurrent reads and fine-grained locked writes (per-bin locking), forbids null keys and values, and offers atomic compound operations — putIfAbsent, compute, merge — that HashMap plus external synchronization cannot match cleanly.

The deeper point is that wrapping a HashMap in Collections.synchronizedMap serializes all access and still leaves check-then-act sequences racy. ConcurrentHashMap's atomic methods exist to close that compound-operation gap without a global lock.

concurrentCounts.merge(key, 1, Integer::sum);   // atomic, no external lock

Interview note: Trap: "is ConcurrentHashMap's size() exact under concurrent updates?" No — it is an estimate; needing an exact live count usually signals a design smell.

How to prepare

Whiteboard put, get and resize until you can narrate them without pausing — that fluency is exactly what the question measures. Then write the failure demos yourself: a key class missing hashCode(), a mutable key you change after insertion, and a map you deliberately pre-size to avoid resizes. Each takes five minutes and permanently inoculates you against the trap versions.

Pair this with the Java coding questions, since hashing underlies the duplicate and frequency problems, and the multithreading questions for the ConcurrentHashMap follow-ups. To rebuild the collections foundations end to end, work through the Java learning path before your next round.

Frequently Asked Questions

Why is HashMap's default load factor 0.75?
0.75 is the tuned trade-off between space and time. A lower load factor leaves more empty buckets, wasting memory but reducing collisions; a higher one packs entries tighter, saving memory but lengthening collision chains and slowing lookups. 0.75 keeps average bucket occupancy low enough for near-O(1) access while keeping memory reasonable, so the table resizes when three-quarters full.
What is treeification in HashMap?
When a single bucket's collision chain grows past 8 nodes and the table has at least 64 buckets, HashMap converts that bucket from a linked list to a red-black tree. This bounds worst-case lookup in that bucket at O(log n) instead of O(n), defending against bad hashCode implementations and deliberate hash-collision attacks. The bucket reverts to a list if it shrinks below 6 nodes.
Why is HashMap's capacity always a power of two?
Because the bucket index is computed as (n-1) & hash, a fast bitwise AND that behaves like modulo only when n is a power of two — then n-1 is all ones in binary and the AND keeps the low bits of the hash. This avoids the cost of a real modulo and makes resize cheap, since on doubling each entry moves to either its old index or old index plus old capacity.
What happens if two keys have the same hashCode?
They land in the same bucket, which is a collision. HashMap then walks the bucket comparing hash and then equals; if a key equals an existing one its value is replaced, otherwise the new entry is appended to the chain (or tree). Equal hashCodes are legal and expected; the map stays correct as long as equal objects have equal hashCodes and equals is consistent.
Is HashMap thread-safe, and what breaks under concurrency?
No. Concurrent writes can corrupt the internal structure and lose updates, and in older Java versions concurrent resize could create an infinite loop in a bucket. For shared access use ConcurrentHashMap, which allows concurrent reads with fine-grained locked writes and provides atomic compound operations like putIfAbsent, compute and merge.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — Explore the Java Full Stack program

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 16 July 2026 LinkedIn
Chat with us