Java Tutorial
🔍

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

OperationHashMapLinkedHashMapTreeMapHashtable
put(k, v)O(1) avgO(1) avgO(log n)O(1) avg + lock
get(k)O(1) avgO(1) avgO(log n)O(1) avg + lock
remove(k)O(1) avgO(1) avgO(log n)O(1) avg + lock
containsKey(k)O(1) avgO(1) avgO(log n)O(1) avg + lock
IterationO(n + capacity)O(n)O(n)O(n + capacity)
Null keys1 allowed1 allowedNot allowedNot allowed
OrderingNoneInsertionSortedNone

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 internally

Mistake 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 operations

Interview 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 falseget() 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