Java HashMap
Java HashMap
java.util.HashMap<K, V> is the collection that answers the question "given this key, what is the value?" in O(1) average time. Product catalogues, user session caches, configuration stores, word frequency counters — anything where you need to look up a value by a meaningful identifier rather than an array index uses HashMap. Understanding how it achieves O(1) lookup internally, and what breaks that guarantee, is one of the most frequently tested topics in Java interviews.
What Is Java HashMap?
HashMap<K, V> is a concrete class in java.util that implements the Map<K, V> interface. A Map stores key-value pairs where each key is unique. HashMap achieves near-constant time for put(), get(), and remove() by converting each key into a bucket index using a hash function, then storing the entry in that bucket.
The diagram below shows where HashMap fits in the Collections hierarchy.
java.util.Map<K, V> ← key-value contract
├── HashMap<K, V> ← THIS CLASS: hash table, no order
│ └── LinkedHashMap<K, V> ← extends HashMap, adds insertion order
├── java.util.SortedMap<K, V>
│ └── NavigableMap<K, V>
│ └── TreeMap<K,V> ← Red-Black tree, sorted by key
└── Hashtable<K, V> ← legacy, synchronised, avoid in new code
KEY FACTS:
Package : java.util
Since : Java 1.2
Implements : Map<K,V>, Cloneable, Serializable
Key ordering : no guaranteed iteration order
Null keys : exactly ONE null key allowed
Null values : unlimited null values allowed
Thread-safe : NO — use ConcurrentHashMap for concurrent access
Default capacity : 16 buckets
Default load factor: 0.75 (rehash when 75% of buckets are occupied)
Basic Overview — The Bucket Array and Entry Nodes
Every entry in a HashMap is stored as a Node<K, V> object inside a Node[] array called table. Each slot in the array is a bucket. Multiple entries can share a bucket — these are called collisions.
HASHMAP INTERNAL STRUCTURE (default capacity 16):
table (Node[] of length 16):
[0]: null
[1]: null
[2]: Node("Mumbai"=2) → null
[3]: Node("Delhi"=1) → Node("Pune"=5) → null ← collision chain
[4]: null
[5]: Node("null"=0) → null ← null key always maps to bucket 0
...
[15]: Node("Bengaluru"=3) → null
Each Node holds:
int hash — pre-computed hash of the key
K key — the key object reference
V value — the value object reference
Node next — pointer to next Node in chain (null if no collision)
JAVA 8 TREEIFICATION:
When a bucket chain grows beyond 8 nodes AND table length >= 64:
The linked list converts to a Red-Black tree (TreeNode)
Lookup improves from O(n) worst-case to O(log n)
When the tree shrinks below 6 entries on removal:
Converts back to a linked list
When to Use HashMap
USE HashMap WHEN:
- Fast key-based lookup is the primary operation (O(1) average)
- Insertion order does not matter
- Sorted order does not matter
- Keys are objects with well-implemented equals() and hashCode()
USE LinkedHashMap WHEN:
- Iteration order must match insertion order
- LRU cache implementation (LinkedHashMap with accessOrder=true)
USE TreeMap WHEN:
- Keys must be in sorted order for iteration
- Range queries are needed: headMap(), tailMap(), floor(), ceiling()
- O(log n) per operation is acceptable
USE ConcurrentHashMap WHEN:
- Multiple threads read and write the same map
- HashMap is not thread-safe — concurrent puts can cause infinite loops
in Java 6 and earlier (due to linked-list cycle during resize)
DO NOT USE Hashtable:
- Synchronises every method, even in single-threaded use — legacy overhead
- Does not allow null keys or values
- ConcurrentHashMap is the correct thread-safe replacement
CHOOSE HashMap over List when:
- You need to look up entries by a meaningful key (name, ID, code)
- list.contains(element) is O(n); map.get(key) is O(1) average
- A list of key-value pairs that you scan with a loop is usually a HashMap
How HashMap Works Internally
Step 1 — The Hash Function
When put(key, value) is called, HashMap first computes the bucket index in two steps.
BUCKET INDEX CALCULATION for key "Delhi":
Step 1: raw hash
h = key.hashCode()
h = "Delhi".hashCode() = -2078581030 (example value)
Step 2: hash spreading — mix high bits into low bits
hash = h ^ (h >>> 16)
This prevents poor hashCode() implementations that only vary in high bits
from clustering all entries into the same few low-indexed buckets.
h = 1111 0000 0101 0100 ... (high bits matter for 16-bucket table)
h >>> 16 = 0000 0000 1111 0000 ...
hash = h ^ (h>>>16) = mixed result with both high and low bits represented
Step 3: bucket index
index = hash & (capacity - 1)
index = hash & 15 (for default capacity 16, mask = 0b1111)
Capacity is always a power of 2, so (capacity-1) is all-ones:
16 - 1 = 15 = 0b00001111
Any hash & 15 produces a value in [0, 15] — perfect bucket spread.
Bitwise AND is faster than modulo (%) for this purpose.
Step 2 — put() Mechanics
put("Delhi", 1) into an empty HashMap:
1. Compute hash for "Delhi" → hash = -2038...
2. index = hash & 15 = 3
3. table[3] is null → create new Node("Delhi", 1) and store at table[3]
put("Pune", 5) — assume also maps to bucket 3 (collision):
1. hash("Pune") & 15 = 3
2. table[3] is not null → traverse chain
3. For each node: hash matches AND "Delhi".equals("Pune")? → false
4. Reach end of chain → append new Node("Pune", 5) as next of "Delhi" node
table[3]: Node("Delhi",1) → Node("Pune",5) → null
put("Delhi", 99) — update existing key:
1. hash("Delhi") & 15 = 3
2. table[3]: check Node("Delhi",1) — hash matches AND "Delhi".equals("Delhi") → true
3. Replace value: node.value = 99
4. Returns old value 1 — put() return value indicates previous value
get("Delhi") — two-step lookup:
1. hash("Delhi") → bucket 3
2. Walk chain: "Delhi".equals("Delhi") → found → return node.value
O(1) average: typically one or two comparisons in a well-sized map.
Step 3 — Resize and Rehash
When size > capacity * loadFactor, HashMap doubles the table and redistributes all entries.
RESIZE TRIGGER at default settings:
capacity = 16, loadFactor = 0.75
threshold = 16 × 0.75 = 12
When size reaches 13: resize fires
RESIZE PROCESS:
1. New table of capacity 32 is allocated
2. For every existing Node, recompute: index = hash & (32 - 1)
— because the mask is now 31 (0b11111) instead of 15 (0b1111)
— each entry either stays in its current slot OR moves to slot + 16
— This is why capacity must always be a power of 2: the redistribution
is a simple one-bit test, not a full re-hash
3. All chains are redistributed into the new table
4. O(n) resize — amortised O(1) per put() over the full lifetime
PRE-SIZING EXAMPLE:
Loading 10,000 entries: each resize doubles capacity (16→32→64→...→16384)
To avoid ~6 resize operations: new HashMap<>(16384) or
new HashMap<>(10000 / 0.75 + 1) ≈ new HashMap<>(13334)
This allocates capacity ≥ 16384 and prevents all resizes.
Java 8 Treeification — The Collision Defense
Before Java 8, a bucket chain that degenerated to n entries caused O(n) lookups — attackers could craft keys with identical hashes to cause HashMap to perform at O(n) on every operation. Java 8 introduced treeification.
TREEIFICATION RULES:
When a single bucket chain length exceeds 8 nodes
AND the table length is >= 64:
→ Convert the linked list to a Red-Black tree (TreeNode)
→ Lookup improves from O(n) to O(log n)
When table length < 64 and a bucket chain exceeds 8:
→ Resize instead of treeify (more buckets = better distribution)
UNTREEIFY:
When a tree bucket shrinks below 6 entries (through remove()):
→ Convert back to a linked list
WHY THIS MATTERS:
String.hashCode() on carefully crafted strings can produce
the same hash value deliberately. Without treeification, a
HashMap<String, ...> is vulnerable to O(n) DoS attacks
through hash flooding. With treeification, the worst case
is O(log n) for any single bucket.
Core Operations with Examples
put(), get(), containsKey(), remove()
1// File: HashMapBasicsDemo.java
2
3import java.util.HashMap;
4import java.util.Map;
5
6public class HashMapBasicsDemo {
7
8 public static void main(String[] args) {
9
10 Map<String, Integer> cityPopulation = new HashMap<>();
11
12 // put() returns the previous value (null if key was absent)
13 System.out.println("=== put() ===");
14 System.out.println("put(Mumbai, 2069) : " + cityPopulation.put("Mumbai", 2069));
15 System.out.println("put(Delhi, 1679) : " + cityPopulation.put("Delhi", 1679));
16 System.out.println("put(Bengaluru, 961) : " + cityPopulation.put("Bengaluru", 961));
17 System.out.println("put(Mumbai, 2100) : " + cityPopulation.put("Mumbai", 2100)); // update — returns old value 2069
18 System.out.println("Map: " + cityPopulation);
19
20 System.out.println("\n=== get() ===");
21 System.out.println("get(Delhi) : " + cityPopulation.get("Delhi")); // 1679
22 System.out.println("get(Chennai) : " + cityPopulation.get("Chennai")); // null — key absent
23
24 // getOrDefault — avoids null check for absent keys
25 System.out.println("getOrDefault(Chennai, 0): "
26 + cityPopulation.getOrDefault("Chennai", 0)); // 0
27
28 System.out.println("\n=== containsKey() and containsValue() ===");
29 System.out.println("containsKey(Delhi) : " + cityPopulation.containsKey("Delhi")); // true
30 System.out.println("containsKey(Hyderabad) : " + cityPopulation.containsKey("Hyderabad")); // false
31 System.out.println("containsValue(961) : " + cityPopulation.containsValue(961)); // true
32
33 System.out.println("\n=== remove() ===");
34 System.out.println("remove(Bengaluru) : " + cityPopulation.remove("Bengaluru")); // 961
35 System.out.println("remove(Hyderabad) : " + cityPopulation.remove("Hyderabad")); // null — was absent
36 System.out.println("remove(Delhi, 999) : " + cityPopulation.remove("Delhi", 999)); // false — value mismatch
37 System.out.println("remove(Delhi, 1679) : " + cityPopulation.remove("Delhi", 1679)); // true — key+value match
38 System.out.println("Map after removes: " + cityPopulation);
39
40 // Null key — HashMap allows exactly one null key
41 System.out.println("\n=== Null key ===");
42 cityPopulation.put(null, 0);
43 System.out.println("get(null) : " + cityPopulation.get(null)); // 0
44 System.out.println("Map: " + cityPopulation);
45 }
46}Output:
=== put() ===
put(Mumbai, 2069) : null
put(Delhi, 1679) : null
put(Bengaluru, 961) : null
put(Mumbai, 2100) : 2069
Map: {Bengaluru=961, Delhi=1679, Mumbai=2100}
=== get() ===
get(Delhi) : 1679
get(Chennai) : null
getOrDefault(Chennai, 0): 0
=== containsKey() and containsValue() ===
containsKey(Delhi) : true
containsKey(Hyderabad) : false
containsValue(961) : true
=== remove() ===
remove(Bengaluru) : 961
remove(Hyderabad) : null
remove(Delhi, 999) : false
remove(Delhi, 1679) : true
Map after removes: {Mumbai=2100}
=== Null key ===
get(null) : 0
Map: {null=0, Mumbai=2100}
Iterating a HashMap
1// File: HashMapIterationDemo.java
2
3import java.util.HashMap;
4import java.util.Map;
5
6public class HashMapIterationDemo {
7
8 public static void main(String[] args) {
9
10 Map<String, Double> productPrices = new HashMap<>();
11 productPrices.put("Laptop", 49999.0);
12 productPrices.put("Mouse", 999.0);
13 productPrices.put("Keyboard", 2499.0);
14 productPrices.put("Monitor", 18999.0);
15 productPrices.put("Webcam", 3499.0);
16
17 // entrySet() — most efficient: key and value in one step
18 System.out.println("=== entrySet() — key + value ===");
19 for (Map.Entry<String, Double> entry : productPrices.entrySet()) {
20 System.out.printf(" %-12s Rs.%7.2f%n", entry.getKey(), entry.getValue());
21 }
22
23 // keySet() — when only keys are needed
24 System.out.println("\n=== keySet() — keys only ===");
25 for (String key : productPrices.keySet()) {
26 System.out.print(key + " ");
27 }
28 System.out.println();
29
30 // values() — when only values are needed
31 System.out.println("\n=== values() — values only ===");
32 double total = 0;
33 for (double price : productPrices.values()) {
34 total += price;
35 }
36 System.out.printf("Total inventory value: Rs.%.2f%n", total);
37
38 // forEach (Java 8+) — cleaner for simple iteration
39 System.out.println("\n=== forEach (Java 8+) — apply discount ===");
40 productPrices.forEach((product, price) ->
41 System.out.printf(" %-12s Rs.%7.2f → Rs.%7.2f (10%% off)%n",
42 product, price, price * 0.9));
43 }
44}Output:
=== entrySet() — key + value ===
Webcam Rs. 3499.00
Laptop Rs. 49999.00
Monitor Rs. 18999.00
Keyboard Rs. 2499.00
Mouse Rs. 999.00
=== keySet() — keys only ===
Webcam Laptop Monitor Keyboard Mouse
=== values() — values only ===
Total inventory value: Rs.75995.00
=== forEach (Java 8+) — apply discount ===
Webcam Rs. 3499.00 → Rs. 3149.10 (10% off)
Laptop Rs. 49999.00 → Rs. 44999.10 (10% off)
Monitor Rs. 18999.00 → Rs. 17099.10 (10% off)
Keyboard Rs. 2499.00 → Rs. 2249.10 (10% off)
Mouse Rs. 999.00 → Rs. 899.10 (10% off)
Java 8 Map Methods — compute, merge, getOrDefault
These methods eliminate boilerplate null-check patterns that appear constantly in production code.
1// File: HashMapJava8Demo.java
2
3import java.util.HashMap;
4import java.util.Map;
5
6public class HashMapJava8Demo {
7
8 public static void main(String[] args) {
9
10 // getOrDefault — word frequency counter without null checks
11 System.out.println("=== Word frequency with getOrDefault ===");
12 String[] words = {"java", "spring", "java", "hashmap", "java", "spring", "hibernate"};
13 Map<String, Integer> freq = new HashMap<>();
14 for (String word : words) {
15 freq.put(word, freq.getOrDefault(word, 0) + 1);
16 }
17 System.out.println("Frequencies: " + freq);
18
19 // computeIfAbsent — initialise only if key is absent
20 System.out.println("\n=== computeIfAbsent — group by category ===");
21 Map<String, java.util.List<String>> grouped = new HashMap<>();
22 String[][] items = {
23 {"Fruits", "Mango"}, {"Veg", "Onion"},
24 {"Fruits", "Banana"}, {"Dairy", "Milk"},
25 {"Veg", "Tomato"}, {"Fruits", "Apple"}
26 };
27 for (String[] item : items) {
28 grouped.computeIfAbsent(item[0], k -> new java.util.ArrayList<>())
29 .add(item[1]); // computeIfAbsent returns existing or new list
30 }
31 grouped.forEach((cat, list) -> System.out.println(" " + cat + ": " + list));
32
33 // merge — combine with existing value, remove if result is null
34 System.out.println("\n=== merge — safe aggregation ===");
35 Map<String, Integer> salesByCity = new HashMap<>();
36 String[] cities = {"Mumbai","Delhi","Mumbai","Pune","Delhi","Mumbai"};
37 for (String city : cities) {
38 salesByCity.merge(city, 1, Integer::sum); // add 1 to existing, or put 1 if absent
39 }
40 System.out.println("Sales by city: " + salesByCity);
41
42 // compute — update a value based on existing content
43 System.out.println("\n=== compute — apply discount to specific key ===");
44 Map<String, Double> prices = new HashMap<>(Map.of("Laptop", 49999.0, "Mouse", 999.0));
45 prices.compute("Laptop", (key, currentPrice) ->
46 currentPrice == null ? 0.0 : currentPrice * 0.9); // 10% discount
47 System.out.println("After compute: " + prices);
48
49 // putIfAbsent — register only if not already set
50 System.out.println("\n=== putIfAbsent — idempotent registration ===");
51 Map<String, String> sessions = new HashMap<>();
52 sessions.put("user-001", "session-A");
53 System.out.println("putIfAbsent(user-001): "
54 + sessions.putIfAbsent("user-001", "session-B")); // session-A — not replaced
55 System.out.println("putIfAbsent(user-002): "
56 + sessions.putIfAbsent("user-002", "session-C")); // null — was absent, inserted
57 System.out.println("Sessions: " + sessions);
58 }
59}Output:
=== Word frequency with getOrDefault ===
Frequencies: {hibernate=1, java=3, spring=2, hashmap=1}
=== computeIfAbsent — group by category ===
Veg: [Onion, Tomato]
Dairy: [Milk]
Fruits: [Mango, Banana, Apple]
=== merge — safe aggregation ===
Sales by city: {Pune=1, Delhi=2, Mumbai=3}
=== compute — apply discount to specific key ===
After compute: {Laptop=44999.1, Mouse=999.0}
=== putIfAbsent — idempotent registration ===
putIfAbsent(user-001): session-A
putIfAbsent(user-002): null
Sessions: {user-001=session-A, user-002=session-C}
Real-World Example — Meesho Seller Session Cache
A session cache at Meesho stores active seller sessions for the API gateway. Incoming requests carry a session token. The gateway looks up the session in O(1) to verify the seller's identity and permissions before forwarding to downstream services. Sessions expire after inactivity and must be evicted. HashMap provides the O(1) lookup; a timestamp field inside the session handles expiry.
1// File: SellerSession.java
2
3public class SellerSession {
4
5 private final String sessionToken;
6 private final String sellerId;
7 private final String sellerName;
8 private final String[] permissions;
9 private long lastAccessedMs;
10
11 public SellerSession(String sessionToken, String sellerId,
12 String sellerName, String... permissions) {
13 this.sessionToken = sessionToken;
14 this.sellerId = sellerId;
15 this.sellerName = sellerName;
16 this.permissions = permissions;
17 this.lastAccessedMs = System.currentTimeMillis();
18 }
19
20 public String getSessionToken() { return sessionToken; }
21 public String getSellerId() { return sellerId; }
22 public String getSellerName() { return sellerName; }
23 public String[] getPermissions() { return permissions; }
24
25 public void touch() { this.lastAccessedMs = System.currentTimeMillis(); }
26
27 public boolean isExpired(long sessionTimeoutMs) {
28 return (System.currentTimeMillis() - lastAccessedMs) > sessionTimeoutMs;
29 }
30
31 public boolean hasPermission(String permission) {
32 for (String p : permissions) {
33 if (p.equals(permission)) return true;
34 }
35 return false;
36 }
37
38 @Override
39 public String toString() {
40 return String.format("Session[seller=%s, token=...%s]",
41 sellerName, sessionToken.substring(sessionToken.length() - 6));
42 }
43}1// File: SessionCache.java
2
3import java.util.HashMap;
4import java.util.Iterator;
5import java.util.Map;
6
7public class SessionCache {
8
9 private static final long SESSION_TIMEOUT_MS = 100; // short for demo
10 private final Map<String, SellerSession> cache = new HashMap<>();
11
12 public void register(SellerSession session) {
13 cache.put(session.getSessionToken(), session); // O(1) insert
14 System.out.println(" REGISTERED: " + session);
15 }
16
17 // O(1) lookup — the core operation the gateway performs on every request
18 public SellerSession authenticate(String token) {
19 SellerSession session = cache.get(token); // O(1) get
20 if (session == null) {
21 System.out.println(" AUTH FAILED : unknown token ..."+token.substring(token.length()-6));
22 return null;
23 }
24 if (session.isExpired(SESSION_TIMEOUT_MS)) {
25 cache.remove(token);
26 System.out.println(" SESSION EXPIRED: " + session);
27 return null;
28 }
29 session.touch(); // refresh last-access time
30 System.out.println(" AUTH OK : " + session
31 + " permissions=" + java.util.Arrays.toString(session.getPermissions()));
32 return session;
33 }
34
35 // Evict all expired sessions — safe removal during iteration using entrySet iterator
36 public void evictExpired() {
37 Iterator<Map.Entry<String, SellerSession>> it = cache.entrySet().iterator();
38 int evicted = 0;
39 while (it.hasNext()) {
40 Map.Entry<String, SellerSession> entry = it.next();
41 if (entry.getValue().isExpired(SESSION_TIMEOUT_MS)) {
42 it.remove(); // safe — iterator.remove() syncs modCount
43 evicted++;
44 System.out.println(" EVICTED: " + entry.getValue());
45 }
46 }
47 System.out.println(" Eviction sweep complete. Evicted: " + evicted
48 + " Remaining: " + cache.size());
49 }
50
51 public int activeSessionCount() { return cache.size(); }
52
53 public static void main(String[] args) throws InterruptedException {
54
55 SessionCache sessionCache = new SessionCache();
56
57 System.out.println("--- Registering seller sessions ---");
58 sessionCache.register(new SellerSession(
59 "tok-SELLER-001-abc123", "S001", "FashionHub", "READ", "WRITE", "PUBLISH"));
60 sessionCache.register(new SellerSession(
61 "tok-SELLER-002-def456", "S002", "ElectroWorld", "READ", "WRITE"));
62 sessionCache.register(new SellerSession(
63 "tok-SELLER-003-ghi789", "S003", "KitchenKing", "READ"));
64
65 System.out.println("\n--- Incoming requests ---");
66 sessionCache.authenticate("tok-SELLER-001-abc123"); // valid
67 sessionCache.authenticate("tok-SELLER-002-def456"); // valid
68 sessionCache.authenticate("tok-UNKNOWN-xyz999"); // unknown token
69
70 Thread.sleep(120); // let some sessions expire
71 System.out.println("\n--- After inactivity period ---");
72 sessionCache.authenticate("tok-SELLER-003-ghi789"); // expired
73 sessionCache.authenticate("tok-SELLER-001-abc123"); // touched earlier — may still be valid
74
75 System.out.println("\n--- Eviction sweep ---");
76 sessionCache.evictExpired();
77
78 System.out.println("\nActive sessions: " + sessionCache.activeSessionCount());
79 }
80}Output:
--- Registering seller sessions ---
REGISTERED: Session[seller=FashionHub, token=...abc123]
REGISTERED: Session[seller=ElectroWorld, token=...def456]
REGISTERED: Session[seller=KitchenKing, token=...ghi789]
--- Incoming requests ---
AUTH OK : Session[seller=FashionHub, token=...abc123] permissions=[READ, WRITE, PUBLISH]
AUTH OK : Session[seller=ElectroWorld, token=...def456] permissions=[READ, WRITE]
AUTH FAILED : unknown token ...z999
--- After inactivity period ---
SESSION EXPIRED: Session[seller=KitchenKing, token=...ghi789]
SESSION EXPIRED: Session[seller=FashionHub, token=...abc123]
--- Eviction sweep ---
EVICTED: Session[seller=ElectroWorld, token=...def456]
Eviction sweep complete. Evicted: 1 Remaining: 0
Active sessions: 0
Performance Considerations
| Operation | HashMap | LinkedHashMap | TreeMap | Hashtable |
|---|---|---|---|---|
| put(k, v) | O(1) avg | O(1) avg | O(log n) | O(1) avg + lock |
| get(k) | O(1) avg | O(1) avg | O(log n) | O(1) avg + lock |
| remove(k) | O(1) avg | O(1) avg | O(log n) | O(1) avg + lock |
| containsKey(k) | O(1) avg | O(1) avg | O(log n) | O(1) avg + lock |
| Iteration | O(n + capacity) | O(n) | O(n) | O(n + capacity) |
| Null keys | 1 allowed | 1 allowed | Not allowed | Not allowed |
| Ordering | None | Insertion | Sorted | None |
O(1) average vs worst case: The O(1) average degrades to O(n) if all keys hash to the same bucket. Java 8 treeification caps this at O(log n) for long chains. A well-distributed hashCode() prevents degradation entirely.
Iteration cost O(n + capacity): HashMap iteration scans all buckets including empty ones. A HashMap with capacity 16,384 but only 5 entries iterates 16,389 slots. LinkedHashMap iterates only the live entries via its linked list — O(n). For sparse maps where iteration is frequent, LinkedHashMap is preferable.
Thread safety: HashMap is not thread-safe. Two threads calling put() simultaneously can corrupt the table — in Java 6 and earlier, concurrent resizing could create a circular linked-list cycle, causing infinite loops. In Java 8+, the resize mechanism is safer but data corruption is still possible. Use ConcurrentHashMap for concurrent access — it uses segment-level locking and provides atomic computeIfAbsent, putIfAbsent, and merge operations.
Best Practices
Pre-size the map when the expected entry count is known. Each resize doubles the table and rehashes all entries — O(n) per resize. For a map that will hold 10,000 entries, use new HashMap<>(10000 / 0.75 + 1) — approximately new HashMap<>(13334). This allocates a table of capacity 16,384 and prevents all resizes during loading. The formula is expectedSize / loadFactor + 1.
Always override both equals() and hashCode() together on custom key classes. HashMap uses hashCode() to find the bucket and equals() to confirm the exact match. Overriding only equals() causes two logically equal keys to land in different buckets — get() returns null for a key that was clearly put(). IDEs generate both methods from the same fields in one action. Use Objects.hash(field1, field2) and Objects.equals(field1, other.field1) for null-safety.
Use entrySet() for iteration, not keySet() followed by get(). for (Entry<K,V> e : map.entrySet()) retrieves both key and value in one step. for (K key : map.keySet()) { V value = map.get(key); } performs a second hash lookup for every key — O(n) extra hash computations. In a map with 100,000 entries, this doubles the iteration cost.
Use Java 8 methods to eliminate null-check boilerplate. getOrDefault(key, default) replaces value = map.get(key); if (value == null) value = default. computeIfAbsent(key, k -> new ArrayList<>()) is cleaner than checking containsKey before creating a new list. merge(key, 1, Integer::sum) is the cleanest word-count pattern — no null check required.
Common Mistakes
Mistake 1 — Using a Mutable Key Whose hashCode Changes After Insertion
1// WRONG — mutable key makes the entry unreachable after mutation
2import java.util.Objects;
3
4class ProductKey {
5 String category;
6 String sku;
7
8 @Override public int hashCode() { return Objects.hash(category, sku); }
9 @Override public boolean equals(Object o) { ... }
10}
11
12Map<ProductKey, String> catalogue = new HashMap<>();
13ProductKey key = new ProductKey("Electronics", "LAPTOP-001");
14catalogue.put(key, "Lenovo ThinkPad");
15
16key.category = "Computers"; // hash code changes — entry is now in wrong bucket!
17
18catalogue.get(key); // null — key hashes to different bucket now
19catalogue.size(); // 1 — entry is still there but unreachable
20
21// FIX: use only immutable fields in equals/hashCode, or use immutable keys (String, Integer)Mistake 2 — Calling get() Inside keySet() Loop Instead of Using entrySet()
1Map<String, Double> prices = new HashMap<>(/* 50,000 entries */);
2
3// WRONG — get() performs a full hash lookup for every key: O(n) extra work
4for (String key : prices.keySet()) {
5 Double price = prices.get(key); // unnecessary second lookup
6 applyDiscount(key, price);
7}
8
9// CORRECT — entrySet() gives key and value in one step
10for (Map.Entry<String, Double> entry : prices.entrySet()) {
11 applyDiscount(entry.getKey(), entry.getValue()); // no extra lookup
12}Mistake 3 — Modifying the Map Directly Inside entrySet() For-Each
1Map<String, Integer> scores = new HashMap<>(Map.of("Alice", 88, "Bob", 42, "Carol", 95));
2
3// WRONG — calling map.remove() inside for-each throws ConcurrentModificationException
4for (Map.Entry<String, Integer> entry : scores.entrySet()) {
5 if (entry.getValue() < 50) {
6 scores.remove(entry.getKey()); // CME — modCount changes outside iterator
7 }
8}
9
10// CORRECT — use iterator.remove() or entrySet().removeIf()
11scores.entrySet().removeIf(entry -> entry.getValue() < 50); // handles modCount internallyMistake 4 — Using HashMap in Multi-threaded Code Without Synchronisation
1// WRONG — shared HashMap with concurrent puts can corrupt the internal table
2Map<String, String> sharedCache = new HashMap<>();
3// Thread 1 and Thread 2 both call sharedCache.put() without synchronisation
4// Result: data loss, NullPointerException, or infinite loop in older JVMs
5
6// CORRECT — use ConcurrentHashMap for thread-safe concurrent access
7Map<String, String> safeCache = new java.util.concurrent.ConcurrentHashMap<>();
8// put(), get(), remove(), computeIfAbsent() are all thread-safe
9// No external synchronisation needed for individual operationsInterview Questions
Q1. How does HashMap work internally in Java?
HashMap stores entries in a Node[] array called table. When put(key, value) is called, it computes an index as hash(key.hashCode()) & (capacity - 1) and stores the entry at that index. If two keys produce the same index (a collision), they are chained as a linked list at that bucket. In Java 8+, when a chain exceeds 8 entries and the table has at least 64 buckets, the chain is converted to a Red-Black tree — improving worst-case lookup from O(n) to O(log n). get(key) uses the same hash to find the bucket, then walks the chain comparing equals() until the matching key is found.
Q2. What is the role of hashCode() and equals() in HashMap?
hashCode() determines which bucket the key maps to — it is the first step in locating an entry. equals() confirms the exact match within the bucket — multiple keys can share a bucket (collisions), so equality checking is needed to find the right one. The contract: if a.equals(b) is true, then a.hashCode() must equal b.hashCode(). If hashCode() is overridden but equals() is not, two logically equal keys land in the same bucket but equals() returns false — get() finds the bucket but not the entry. If equals() is overridden but hashCode() is not, equal keys land in different buckets — get() looks in the wrong bucket entirely.
Q3. What is the default load factor of HashMap and why is 0.75 the default?
The default load factor is 0.75, meaning HashMap resizes when 75% of its buckets are occupied. The value 0.75 is a balance between two competing costs: a lower load factor (e.g., 0.5) keeps buckets sparse — fewer collisions, faster lookup — but wastes more memory. A higher load factor (e.g., 0.9) uses memory more efficiently but increases collision chains, degrading average lookup time. 0.75 was empirically shown to provide good throughput and acceptable space overhead under typical key distribution patterns.
Q4. What is treeification in HashMap and why was it introduced in Java 8?
Treeification converts a bucket's linked list into a Red-Black tree when the chain length exceeds 8 entries and the table size is at least 64. Before Java 8, an attacker could craft a set of keys with identical hashCode() values, forcing all entries into one bucket and degrading every get() and put() to O(n). With treeification, even adversarial hash distributions produce O(log n) per operation. The tree reverts to a linked list when it shrinks below 6 entries. This change made HashMap resistant to hash-flooding denial-of-service attacks without sacrificing average-case O(1) performance.
Q5. What is the difference between HashMap and ConcurrentHashMap?
HashMap is not thread-safe. Concurrent puts from two threads can corrupt the internal table — in Java 6, concurrent resizing could create a circular reference in the linked list, causing an infinite loop. ConcurrentHashMap divides the map into segments (Java 7) or uses CAS and bucket-level synchronisation (Java 8+), allowing multiple threads to read and write without blocking each other unnecessarily. ConcurrentHashMap also provides atomic compound operations: putIfAbsent(), computeIfAbsent(), and merge() — essential for race-condition-free patterns like counters and caches. It does not allow null keys or values. Collections.synchronizedMap(new HashMap<>()) exists but synchronises on the entire map object — much slower under concurrent load.
Q6. Why should HashMap keys be immutable?
HashMap computes the bucket index from key.hashCode() at the time of put(). If the key is mutable and a field used in hashCode() changes after insertion, the entry is now stored in bucket A but its new hash points to bucket B. Any subsequent get() with the same key lands in bucket B, finds nothing, and returns null. The entry is effectively lost — size() still counts it but it can never be retrieved or removed through normal operations. Using immutable keys — String, Integer, UUID, or a custom immutable record — eliminates this bug entirely.
FAQs
What is the default initial capacity of HashMap in Java?
The default initial capacity is 16 buckets with a load factor of 0.75. The first resize fires when size exceeds 12 (16 × 0.75). The constructor new HashMap<>(initialCapacity) lets you specify a different starting capacity, which is rounded up to the next power of 2. For known sizes, pre-sizing avoids resize overhead: new HashMap<>(expectedSize / 0.75 + 1).
Can HashMap have duplicate keys?
No. If you put() a key that already exists, the new value replaces the old one. The old value is returned by put(). HashMap can have duplicate values — multiple keys can map to the same value — but each key appears exactly once.
What is the difference between HashMap and Hashtable?
Hashtable is the legacy synchronised map from Java 1.0. Every method acquires a mutex, even in single-threaded code. It does not allow null keys or values. HashMap is faster (no synchronisation), allows one null key and unlimited null values, and was introduced in Java 1.2 as the non-synchronised replacement. For concurrent use, ConcurrentHashMap is the correct modern choice — not Hashtable.
How does HashMap handle a null key?
HashMap places the null key in bucket 0, bypassing the hashCode() call entirely (which would throw NullPointerException for a null object). There can be at most one null key. get(null) looks specifically in bucket 0. TreeMap and Hashtable do not support null keys — TreeMap calls compareTo(null), which throws, and Hashtable has an explicit null check.
What happens when two keys have the same hashCode but are not equal?
This is a hash collision. Both entries are stored in the same bucket as a linked list (or tree, if the chain is long enough to trigger treeification). get() walks the chain, calling equals() on each entry until the correct key is found. Collisions are normal and expected — they degrade O(1) average to O(k) where k is the chain length for that bucket. Good hashCode() implementations spread keys evenly across buckets to keep chains short.
Does HashMap maintain insertion order?
No. HashMap makes no guarantee about iteration order — entries may appear in any order, and the order can change after a resize. If insertion order is required, use LinkedHashMap, which maintains a doubly-linked list through all entries in insertion sequence. If sorted key order is required, use TreeMap.
Summary
HashMap<K, V> achieves O(1) average put(), get(), and remove() by converting each key to a bucket index via hash(key.hashCode()) & (capacity - 1). Collisions are handled by separate chaining — linked lists that treeify to Red-Black trees when a bucket exceeds 8 entries in Java 8+. Resize fires at 75% capacity and doubles the table.
Two rules govern correctness: override both equals() and hashCode() on key classes using the same fields, and never mutate those fields after insertion. Violating either produces lost entries that size() counts but get() cannot retrieve.
For interviews: explain the two-step lookup (hashCode → bucket, equals → node), describe the load factor and resize trigger, know the difference between HashMap and ConcurrentHashMap for thread safety, and describe treeification and why it was introduced. These questions cover everything from TCS fresher rounds through Flipkart and Razorpay senior rounds.
What to Read Next
Learn a HashMap that also remembers insertion order.