DevNotes
Support
Collection Framework · Chapter 15

HashMap

HashMap from the inside — Node<K,V>, hash spreading, (n-1) & hash, put/get flows, collisions, treeification, load factor and resizing.

◷ 19 min read§ 22 sections? 11 interview questions{ } 30 code examples

#Introduction

HashMap is one of the most important Java interview topics — the implementation you should understand internally, not just at API level. The central idea:

HashMap converts a key into a hash, uses that hash to locate a bucket, and then uses equality to identify the exact key.

We'll build the internals without repeating the basic Map concepts from the Map Interface chapter.

#HashMap at a Glance

Java
Map<Integer, String> map = new HashMap<>();

map.put(101, "Java");
map.put(102, "Spring");
HashMap
Hash Table
Bucket
Entry102 → Spring
OperationAverageWorst / pathological
put()O(1)O(log n) with tree bins
get()O(1)O(log n) with tree bins
remove()O(1)O(log n) with tree bins
containsKey()O(1)O(log n) with tree bins

The O(1) figure assumes good hash distribution.

#What Does HashMap Actually Store?

Internally, HashMap maintains an array called the table:

Java
Node<K,V>[] table;

Each occupied bucket contains a chain/tree of nodes. A simplified Node looks like:

Java
static class Node<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}

So each entry contains hash, key, value, next. The next reference allows multiple entries to exist in the same bucket.

HashMap Internal Structure

Java
map.put("A", 100);
map.put("B", 200);
map.put("C", 300);
Bucket 0
empty
Bucket 1
[A100]
Bucket 2
[B200]
Bucket 3
[C300]
table

If two keys land in the same bucket, they are chained: bucket 2 → [A,100] → [B,200]. That's a hash collision.

#The put() Journey

When you execute map.put(key, value); think:

put()
Calculate hash
Calculate bucket index
Check bucket
Empty?
Insert
Compare existing keys
Compare existing keys
Same key?
Replace value
Collision handling
If the bucket becomes sufficiently crowded, it may be converted from a linked structure to a tree

#Step 1 — Calculate the Hash

For a key like "Java", HashMap obtains key.hashCode(). But modern HashMap doesn't simply use that value directly — it performs additional bit mixing.

Hash Spreading

Conceptually, Java 8+ HashMap uses h ^ (h >>> 16). The implementation's hash method is essentially:

Java
static final int hash(Object key) {
    int h;
    return (key == null)
            ? 0
            : (h = key.hashCode()) ^ (h >>> 16);
}

Why? Because the bucket index uses only part of the hash. Mixing high bits into low bits helps improve distribution.

Why Mix the Hash?

If a key's hashCode() has poor variation in its low bits, many keys can land in the same bucket.

Original hash
Mix high bits into low bits
Better bucket distribution
This reduces unnecessary collisions

Null-Key Special Case

HashMap permits one null key:

Java
map.put(null, "Hello");

HashMap assigns hash = 0 to a null key, so null handling doesn't require calling null.hashCode() — which obviously isn't possible.

#Step 2 — Calculate the Bucket Index

For a table of size n:

Java
index = (n - 1) & hash;

This is one of the most frequently asked HashMap questions. Why not hash % n? Because bitwise AND is efficient when n is a power of two.

Why Capacity Is a Power of 2

Typical capacities: 16, 32, 64, 128, 256, …

Suppose n = 16 → n - 1 = 15 → binary 1111. Therefore hash & 1111 efficiently extracts the relevant lower bits. This is the basis for (hash & (n - 1)).

Why Not Any Arbitrary Capacity?

nn − 1BinaryMask quality
16151111Clean bit mask
1091001Distribution properties not equivalent

HashMap therefore uses power-of-two table sizes.

#Step 3 — Find the Bucket

After calculating index = (n - 1) & hash, HashMap accesses table[index]. If table[index] == null, the entry can be inserted directly. If not, collision handling begins.

#Collisions and Treeification

Collision

A collision occurs when different keys map to the same bucket — e.g. Key A → bucket 5 and Key B → bucket 5. Even if A.equals(B) == false, they can still share a bucket.

Collision Handling

Modern HashMap can use a linked list or a Red-Black tree inside a bucket.

A
B
C
Initially: bucket 5 holds a chain

If the collision chain becomes large enough, bucket 5 may become a Red-Black Tree. This is called treeification.

Why Treeify?

Suppose one bucket has many entries: A → B → C → D → E → F → G → H. Searching a long linked list can approach O(n); a balanced tree provides approximately O(log n) within that bucket. So treeification improves worst-case collision behaviour.

Treeification Threshold

Java
TREEIFY_THRESHOLD = 8

When a bucket's node count reaches the relevant threshold, HashMap may attempt to treeify the bucket.

Minimum Treeify Capacity

Java
MIN_TREEIFY_CAPACITY = 64

If the table is too small, HashMap prefers resizing rather than treeifying.

Bucket becomes crowded
Capacity < 64?
Resize
Treeify
This distinction is frequently tested

Why Resize Instead of Treeify?

A small table (e.g. size 16 with many entries) can naturally create collisions simply because there aren't enough buckets. Rather than immediately creating trees, increasing the number of buckets spreads the entries out. So small table + collisions → resize first is generally more efficient.

Untreeification

A tree bin doesn't necessarily remain a tree forever. If entries are removed and the number falls sufficiently, HashMap can convert the tree back into a linked structure.

Java
UNTREEIFY_THRESHOLD = 6
Many collisions
Tree
Entries removed
Small enough
Linked structure
The exact transition logic includes implementation details beyond simply comparing one number

#Load Factor, Capacity and Threshold

Load Factor

Default load factor: 0.75f. Suppose capacity = 16, load factor = 0.75:

Text
Threshold = 16 × 0.75 = 12

When the number of entries reaches the resize threshold, HashMap expands.

What Does Load Factor Mean?

Load factor controls the trade-off between memory and collision frequency.

Load factorBucketsCollisionsMemory
LowerMoreFewerMore
HigherFewerPotentially moreLess

The default 0.75 is a practical compromise.

Capacity vs Size vs Threshold

These are often confused.

TermMeaningExample
CapacityNumber of buckets16
SizeNumber of mappings currently stored10
ThresholdApproximate size at which resizing occurs: capacity × loadFactor12

#Resizing

What Happens During Resize?

Suppose capacity = 16, threshold = 12, and HashMap needs to resize. The table becomes 16 → 32. Generally, HashMap doubles its capacity, and the threshold is recalculated accordingly.

Why Double the Capacity?

Doubling gives HashMap a convenient property. If the old capacity is 16 and the new capacity is 32, an existing entry generally either stays at the same index or moves by +16. This is determined by a particular hash bit, which makes resizing more efficient than recomputing arbitrary bucket positions from scratch.

The Resize Bit Trick

Old capacity 16, new capacity 32 — the new bit being introduced is 16. For each entry, conceptually:

Java
if ((hash & oldCapacity) == 0) {
    // same index
} else {
    // old index + old capacity
}

This is one reason power-of-two capacities are so useful.

Does Rehashing Call hashCode() Again?

This is a subtle interview question. During resize, HashMap generally does not need to call the key's hashCode() again for every entry. The stored hash is already available in each node; it uses the old hash and the capacity bit to determine the new position.

#The get() Journey

Java
map.get("Java");
get(key)
Calculate hash
Calculate bucket index
Check first node
hash matches? → equals()?
Found?
return value
continue in bucket (tree search or node traversal)

If the bucket is a tree, HashMap searches the Red-Black Tree; if it's a linked structure, it traverses the nodes.

Why Both hash and equals()?

If two keys have different hash codes, they cannot be the same logical key in a correctly implemented hash-based Map. So HashMap can first check the hash before calling equals(), which avoids unnecessary equality comparisons.

  • Hash differs → not the key.
  • Hash same → check equals().

#The equals() / hashCode() Contract

This is one of the most important HashMap concepts. If a.equals(b) is true, then a.hashCode() == b.hashCode() must be true. The reverse isn't required: two different objects can have the same hash (hashCode() same, equals() false) — that's a collision.

Wrong hashCode() Implementation

Java
class Employee {

    int id;

    @Override
    public boolean equals(Object o) {
        // ...
    }
}

If hashCode() isn't overridden consistently, two logically equal Employees may have different hashes, and HashMap can place them into different buckets. Result: equals() says same, HashMap lookup says different. This violates the required contract.

Correct Implementation

Java
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Employee other)) return false;
    return id == other.id;
}

@Override
public int hashCode() {
    return Integer.hashCode(id);
}

Now: same id → equals() true → same hash.

Mutable Keys — Major Trap

Java
Map<Employee, String> map = new HashMap<>();

Employee e = new Employee(101);
map.put(e, "John");

e.setId(999);

If id participates in hashCode(): old hash → bucket A becomes new hash → bucket B, but the entry is physically still in its original bucket. Now map.get(e) may fail.

#Thread Safety and Iteration

Why HashMap Isn't Thread-Safe

Two threads calling put() at the same time may both modify internal state concurrently. Without appropriate synchronization, operations can interfere with each other, and visibility/ordering guarantees aren't provided. Therefore HashMap is not a concurrent Map. For concurrent access, ConcurrentHashMap is usually the appropriate choice.

HashMap vs ConcurrentHashMap

FeatureHashMapConcurrentHashMap
Thread-safe❌ No✅ Yes
Null key✅ One❌ No
Null values✅ Yes❌ No
Concurrent operations❌ No✅ Yes
General useYesConcurrent applications

We'll deep-dive into ConcurrentHashMap later.

Fail-Fast Iterator

HashMap iterators are generally fail-fast.

Java
for (Integer key : map.keySet()) {
    if (key == 10) {
        map.remove(key);
    }
}
Output
ConcurrentModificationException

because the Map was structurally modified outside the iterator.

Correct Iterator Removal

Java
Iterator<Integer> iterator = map.keySet().iterator();

while (iterator.hasNext()) {
    Integer key = iterator.next();
    if (key == 10) {
        iterator.remove();
    }
}

The iterator knows about its own removal operation.

HashMap Doesn't Guarantee Ordering

Java
Map<Integer, String> map = new HashMap<>();

map.put(3, "C");
map.put(1, "A");
map.put(2, "B");

You cannot write code assuming the iteration will be 3, 1, 2 or 1, 2, 3. Neither ordering should be relied upon. For insertion order use LinkedHashMap; for sorted keys use TreeMap.

#Null Handling

HashMap permits one null key and multiple null values.

Java
map.put(null, "A");
map.put(null, "B");

Result: null → B. Only one null key exists because keys are unique.

Why Does HashMap Allow Null but ConcurrentHashMap Doesn't?

HashMap can represent key → null without needing concurrent atomic semantics. ConcurrentHashMap deliberately disallows null because concurrent operations need a clear distinction between key absent and key mapped to null. This simplifies its concurrent API semantics.

#Constructors and Capacity

Constructors

Java
new HashMap<>();
new HashMap<>(32);
new HashMap<>(32, 0.75f);

Parameters: initialCapacity and loadFactor.

Initial Capacity Is Not Allocated Immediately

Creating new HashMap<>(100); doesn't necessarily mean an array of 100 buckets is immediately allocated. HashMap table allocation is lazy and occurs when needed, such as during the first insertion. Requested capacities are also normalized to HashMap's power-of-two sizing rules.

Default Capacity

The commonly documented default initial capacity is 16 with load factor 0.75. Remember that the internal table is lazily allocated, so the backing array isn't necessarily created at constructor invocation.

Initial Capacity and Performance

If you know approximately how many entries will be stored, providing an appropriate initial capacity can reduce repeated resizing:

Java
Map<Integer, Employee> map = new HashMap<>(1000);

But don't blindly over-allocate — more buckets mean more memory.

#HashMap's Core Formulas

This deserves memorization.

ConceptFormula / constant
Hash spreadingh ^ (h >>> 16)
Bucket index(n - 1) & hash
Resize thresholdcapacity × loadFactor
Default load factor0.75
TreeificationTREEIFY_THRESHOLD = 8
Minimum treeify capacityMIN_TREEIFY_CAPACITY = 64
Untreeification thresholdUNTREEIFY_THRESHOLD = 6

Complete put() Mental Model

put(key, value)
hash(key) → spread hash bits
bucket = hash & (n-1)
table[bucket]
Empty or occupied?
insert
compare keys
Compare keys
Same key?
replace value
add to collision chain (list or tree)
size > threshold? → resize

Complete get() Mental Model

get(key)
calculate hash
calculate bucket
table[index]
check candidate: hash + equals()
Found?
return value
return null

#Performance in Practice

Why HashMap Is Fast

HashMap avoids searching every entry:

Key
Hash
Bucket
Small number of candidates
Equality check
With a good hash distribution: O(1) average

That's the fundamental reason HashMap is so widely used.

Poor Hash Distribution

Suppose every key returns:

Java
@Override
public int hashCode() {
    return 1;
}

Then A, B, C, D … all go to bucket X. Everything goes into one bucket and HashMap loses much of its expected performance advantage. With modern treeification, sufficiently large collision bins can become trees, but a pathological hash function is still a poor design.

Same Hash ≠ Same Key

A classic interview question: A.hashCode() = 100 and B.hashCode() = 100 — does HashMap treat A and B as the same key? No. It checks hash + equals(). If A.equals(B) == false, both entries can coexist.

HashMap and Object Identity

HashMap normally uses logical equality (equals()), not reference identity (==). Therefore two new String("Java") objects represent the same logical key if equals() returns true and their hashes agree. For identity-based key semantics, Java provides IdentityHashMap.

Why containsKey() Is Better Than get() != null

Java
map.put("A", null);

Then map.get("A") == null is true — but the key exists. Therefore map.containsKey("A") is the correct test when you specifically want to know whether the key exists.

putIfAbsent() vs put()

MethodBehaviour
map.put("A", 10)Insert or replace — always updates the mapping
map.putIfAbsent("A", 20)Insert only when absent — doesn't overwrite an existing mapping

Memory Model

HashMap has memory overhead from the table array + Node objects + references + hash values. Compared with a simple array, it's substantially more memory-intensive — but that trade-off buys fast key-based lookup.

HashMap vs ArrayList Lookup

To find the Employee with ID 5000:

  • ArrayList → iterate through elements → potentially O(n)
  • HashMap → employeeId → hash → bucket → employee → O(1) average
Java
Map<Long, Employee> employees = new HashMap<>();

Employee employee = employees.get(10025L);

This is much more appropriate than repeatedly scanning a List when lookup by ID is the primary operation.

#Java 7 vs Java 8+ HashMap

This is a common senior-level interview topic.

  • Older Java (Java 7-era): collision buckets were linked structures. Heavy collisions could lead to poor worst-case lookup behaviour.
  • Java 8+: HashMap introduced treeification of sufficiently large collision bins.
Java 7-era
bucket → linked list
Java 8+
bucket → linked list
Red-Black Tree

This significantly improves pathological collision behaviour (a bucket chain of A → … → H drops from O(n) to O(log n) search).

Does HashMap Always Use Red-Black Trees?

No. Only sufficiently crowded bins can become tree bins. Most normal buckets are either empty, a single node, or a small linked structure. Tree bins are for unusually collision-heavy situations.

#Interview Scenarios

Q1Two different keys have the same hash code. What happens?

Strong answer: They can land in the same bucket. HashMap then distinguishes them using key equality. The bucket may use a linked structure, and if collisions become sufficiently large and the table is large enough, the bin can be treeified into a Red-Black Tree.

Q2If two keys have the same hash code, are they considered duplicates?

No. A hash collision only means they map to the same bucket. HashMap still uses equality to determine whether they represent the same key.

Q3Why shouldn't you use a mutable object as a HashMap key?

If fields used by equals() or hashCode() change after insertion, the key's calculated bucket can change while the entry remains in its original location, making lookup or removal unreliable.

Q4Why is the default load factor 0.75?

It is a practical balance between memory usage and collision frequency. Lower values use more memory but generally reduce collisions; higher values save memory but can increase collisions.

Q5Why does HashMap use power-of-two capacities?

Because bucket selection can efficiently use (n - 1) & hash, and resizing by doubling gives an efficient redistribution rule where entries generally stay at the same index or move by the old capacity.

Q6Why doesn't HashMap immediately treeify a bucket when collisions begin?

Because small collision chains are cheap, and when the overall table is small, resizing can distribute entries more effectively. HashMap therefore uses a minimum capacity threshold before treeification.

Q7Can multiple threads safely modify a HashMap?

Not without external synchronization or another concurrency mechanism. HashMap itself is not thread-safe. For concurrent access, ConcurrentHashMap is usually the appropriate implementation.

#Coding Questions

Q1Duplicate key
Java
Map<String, Integer> map = new HashMap<>();

map.put("A", 10);
map.put("A", 20);

System.out.println(map.size());
System.out.println(map.get("A"));
Output
1
20

The second put() replaces the value for the same logical key.

Q2Collision with different keys
Java
class Key {

    int id;

    @Override
    public int hashCode() {
        return 1;
    }

    @Override
    public boolean equals(Object obj) {
        Key other = (Key) obj;
        return id == other.id;
    }
}
Java
map.put(new Key(1), "A");
map.put(new Key(2), "B");

Can both entries exist? Yes. Both have hashCode = 1, but Key(1).equals(Key(2)) == false, so they are different keys.

Q3Mutable key
Java
Employee e = new Employee(101);

map.put(e, "John");

e.setId(999);

System.out.println(map.get(e));

Don't assume this will return "John". If id determines hashCode(), changing it after insertion can make the lookup fail.

Q4Null key
Java
Map<String, String> map = new HashMap<>();

map.put(null, "A");
map.put(null, "B");

System.out.println(map.size());
System.out.println(map.get(null));
Output
1
B

One null key; the second insertion replaces its value.

#HashMap Interview Cheat Sheet

Memorize this:

TopicAnswer
StructureHash table
Key →hash
Hash spreadingh ^ (h >>> 16)
Bucket(n - 1) & hash
CollisionLinked structure; tree bin when appropriate
Default load factor0.75
Treeify threshold8
Minimum treeify capacity64
Untreeify threshold6
ResizeRoughly doubles capacity
Average lookupO(1)
Thread-safeNo

The Most Important HashMap Formula

If the interviewer asks "How does HashMap find the bucket?":

Java
int hash = key.hashCode();
hash = hash ^ (hash >>> 16);

int index = (n - 1) & hash;

Conceptually, that's the important flow. Don't overstate it as literally the complete implementation for every Java version.

HashMap vs LinkedHashMap vs TreeMap (implementation view)

MapStructureComplexityOrdering
HashMapHash tableO(1) averageNo guarantee
LinkedHashMapHash table + linked ordering structureO(1) averagePredictable iteration order
TreeMapRed-Black TreeO(log n)Sorted keys

Final Decision Framework

NeedChoose
Fast lookupHashMap
Insertion orderLinkedHashMap
Sorted keysTreeMap
Concurrent key-value accessConcurrentHashMap
Enum keysEnumMap
Identity comparisonIdentityHashMap
Weak key referencesWeakHashMap

Final Mental Model

Key
hashCode()
spread hash
bucket in table[]
Collision?
single Node
Node → Node → Node (tree if enough collisions)
hash comparison → equals() → value
That sequence is the heart of HashMap

#Chapter Summary

ConceptKey Point
Internal structureHash table
StorageArray of buckets
EntryNode<K,V>
Hash spreadingh ^ (h >>> 16)
Bucket index(n - 1) & hash
Default capacity16
Default load factor0.75
Thresholdcapacity × load factor
CollisionMultiple keys in same bucket
Collision structureLinked nodes / tree bins
Treeify threshold8
Minimum treeify capacity64
Untreeify threshold6
ResizeCapacity generally doubles
Average get() / put()O(1)
Tree-bin lookupO(log n)
Null keyOne allowed
Null valuesAllowed
OrderingNo guarantee
Thread-safeNo
IteratorGenerally fail-fast

Senior-Level Interview Checklist

  • HashMap internal architecture
  • What Node<K,V> contains
  • hashCode() → hash spreading → bucket index
  • Why (n - 1) & hash is used
  • Why capacity is a power of two
  • What a hash collision is
  • Why same hash doesn't mean same key
  • Role of equals() and the equals()/hashCode() contract
  • HashMap put() and get() flows
  • Load factor, threshold and resizing
  • Why capacity doubles and what happens to entries during resize
  • Treeification: TREEIFY_THRESHOLD, MIN_TREEIFY_CAPACITY, UNTREEIFY_THRESHOLD
  • Why HashMap isn't thread-safe
  • Fail-fast iterators
  • Mutable-key problem
  • Java 7 vs Java 8+ collision handling
  • HashMap vs LinkedHashMap / TreeMap / ConcurrentHashMap