Mid-level 9 min · March 05, 2026

Java HashMap — Non-Atomic Resize Causes Infinite Loops

Rate limit counters corrupted as HashMap resize in concurrent put() triggered infinite loops.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • HashMap stores key-value pairs using hashing for O(1) average lookups
  • Internally uses array of buckets, converts to tree when collisions exceed 8
  • Must override hashCode() and equals() for custom keys
  • Not thread-safe — ConcurrentHashMap is the production replacement
  • load factor 0.75 balances memory and speed; pre-size to avoid rehashing
  • Null keys allowed (stored in bucket 0), null values allowed
Plain-English First

Imagine a massive cloakroom at a concert venue. When you hand over your jacket, the attendant gives you a numbered ticket — say, ticket 42. Later, you hand back ticket 42 and instantly get your jacket, no searching required. A HashMap works exactly like that: you store something under a 'key' (the ticket number), and retrieving it later is near-instant because Java uses that key to jump straight to the right spot. No looping through everything. Just give the key, get the value.

Every real application manages data — user sessions, product prices, word counts, config settings. The moment you need to look something up by a label rather than a position, a plain array or list starts to hurt. You're stuck looping through every element hoping to find a match, and as your data grows, so does your wait time. Java's HashMap is built to eliminate exactly that pain.

How HashMap Actually Stores Data Internally (The Part Most Tutorials Skip)

When you call put(key, value), Java doesn't just dump your data into a list. It calls hashCode() on the key, applies a secondary hash to spread values evenly, then uses the result to pick a 'bucket' — essentially a slot in an internal array. Think of it like sorting mail into pigeonholes: every letter (key-value pair) goes to a specific hole, so retrieval is O(1) on average.

But what if two keys land in the same bucket? That's a hash collision. Java handles this with a linked list inside the bucket. From Java 8 onwards, if one bucket accumulates more than 8 entries, Java converts that list into a red-black tree, dropping worst-case lookup from O(n) to O(log n). This is why your HashMap stays fast even when things get crowded.

Understanding this matters the moment you write a custom class as a key. If your hashCode() is bad — say, always returning 1 — everything piles into a single bucket and your 'fast' map becomes a slow list in disguise.

HashMapInternalsDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.util.HashMap;
import java.util.Map;

public class HashMapInternalsDemo {

    public static void main(String[] args) {

        // A product price catalogue — a perfect real-world HashMap use case
        Map<String, Double> productPrices = new HashMap<>();

        // put() computes hashCode("Laptop"), finds the right bucket, stores the pair
        productPrices.put("Laptop", 999.99);
        productPrices.put("Headphones", 149.49);
        productPrices.put("USB-C Hub", 39.95);
        productPrices.put("Webcam", 89.00);

        // get() uses the same hash logic to jump straight to the value — no loop
        double laptopPrice = productPrices.get("Laptop");
        System.out.println("Laptop price: $" + laptopPrice);

        // getOrDefault() is safer — avoids NullPointerException if key is missing
        double micPrice = productPrices.getOrDefault("Microphone", 0.0);
        System.out.println("Microphone price (not in catalogue): $" + micPrice);

        // containsKey() is O(1) — use it to check before acting
        if (productPrices.containsKey("Webcam")) {
            System.out.println("Webcam is stocked.");
        }

        // Iterating over entries — entrySet() is the most efficient way
        System.out.println("\n--- Full Product Catalogue ---");
        for (Map.Entry<String, Double> entry : productPrices.entrySet()) {
            System.out.printf("%-15s $%.2f%n", entry.getKey(), entry.getValue());
        }

        // HashMap size grows dynamically — default initial capacity is 16, load factor 0.75
        System.out.println("\nTotal products: " + productPrices.size());
    }
}
Output
Laptop price: $999.99
Microphone price (not in catalogue): $0.0
Webcam is stocked.
--- Full Product Catalogue ---
USB-C Hub $39.95
Laptop $999.99
Webcam $89.00
Headphones $149.49
Total products: 4
Why the output order is different from insertion order:
HashMap does NOT guarantee insertion order — keys are ordered by their hash bucket position, which feels arbitrary. If order matters (e.g., displaying a menu), use LinkedHashMap, which maintains insertion order with almost no performance cost.
Production Insight
A hashCode() that returns a constant kills performance — all keys go to one bucket, turning O(1) into O(n).
Java 8's tree conversion only helps if collision bucket size exceeds 8, but the threshold itself adds overhead.
Rule: if you see CPU spikes on Map operations, check hashCode() distribution with a profiler.
Key Takeaway
A bad hashCode() is the #1 performance killer in HashMap.
Collisions are graceful but expensive when keys cluster.
Always write a good hash or use immutable JDK keys.
When to worry about hashCode quality
IfCustom class used as key with many distinct instances
UseImplement a good hashCode() using all immutable fields, prefer Objects.hash()
IfKey space small (e.g., 3-4 enum-like values)
UseEven poor hashCode() is fine; collisions are rare
IfKey values known to be unique strings (e.g., UUID)
UseString.hashCode() is well-distributed, no custom key needed
IfProduction app showing high CPU in HashMap.get()
UseProfile to see if any bucket has high collision count (JDK 8+: use -XX:+PrintHeapAtGC to see treeification)

The hashCode and equals Contract — Why Breaking It Destroys Your Map

If you ever use a custom object as a HashMap key, you must override both hashCode() and equals(). This is non-negotiable, and it's the most common advanced mistake people make.

Here's the rule: if two objects are equal according to equals(), they MUST return the same hashCode(). If you break this, Java puts the same logical key into different buckets and you end up with duplicate entries or, worse, you can never retrieve your data again.

The reverse is fine — two different objects can share a hash code (collision), but equal objects must share a hash. IDE plugins like IntelliJ and Eclipse can auto-generate correct implementations. Always use them, or use Java's built-in Objects.hash() utility which does the heavy lifting safely.

This contract is also why String and Integer work perfectly as HashMap keys out of the box — their hashCode() and equals() are already correctly implemented in the JDK.

CustomKeyHashMapDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

// Imagine a system tracking inventory per warehouse location
class WarehouseLocation {
    private final String city;
    private final int aisle;

    public WarehouseLocation(String city, int aisle) {\n        this.city = city;\n        this.aisle = aisle;\n    }

    // Without overriding equals(), two WarehouseLocation("London", 3) objects
    // are NOT considered equal — Java uses reference equality by default
    @Override
    public boolean equals(Object other) {
        if (this == other) return true;
        if (!(other instanceof WarehouseLocation)) return false;
        WarehouseLocation that = (WarehouseLocation) other;
        return this.aisle == that.aisle && Objects.equals(this.city, that.city);
    }

    // Without overriding hashCode(), equal objects can land in DIFFERENT buckets
    // Objects.hash() combines fields safely into a well-distributed hash code
    @Override
    public int hashCode() {
        return Objects.hash(city, aisle);
    }

    @Override
    public String toString() {
        return city + ", Aisle " + aisle;
    }
}

public class CustomKeyHashMapDemo {\n\n    public static void main(String[] args) {\n\n        Map<WarehouseLocation, Integer> stockLevels = new HashMap<>();\n\n        WarehouseLocation londonAisle3 = new WarehouseLocation(\"London\", 3);\n        stockLevels.put(londonAisle3, 250);\n\n        // This is a DIFFERENT object in memory, but logically the same location\n        WarehouseLocation sameLocation = new WarehouseLocation(\"London\", 3);\n\n        // This works ONLY because we correctly overrode hashCode() and equals()\n        Integer stock = stockLevels.get(sameLocation);\n        System.out.println(\"Stock at \" + sameLocation + \": \" + stock + \" units\");\n\n        // Updating stock — put() with an existing key replaces the old value\n        stockLevels.put(londonAisle3, 180);\n        System.out.println(\"Updated stock: \" + stockLevels.get(sameLocation) + \" units\");\n\n        System.out.println(\"Map size (should be 1, not 2): \" + stockLevels.size());\n    }\n}",
        "output": "Stock at London, Aisle 3: 250 units\nUpdated stock: 180 units\nMap size (should be 1, not 2): 1"
      }

HashMap Resizing and Load Factor — Why Initial Capacity Matters in Production

HashMaps start with a default initial capacity of 16 buckets and a load factor of 0.75. When the number of entries exceeds capacity × load factor (16 × 0.75 = 12), the map resizes to double its current size (32, then 64, etc.). Resizing is expensive: it allocates a new array and rehashes every existing entry into the new buckets. During this process, all concurrent access to the map can cause corruption if not synchronized.

In production, resizing is often a hidden performance cost. If you're loading 10,000 entries into a HashMap with default settings, it will resize about 9 times (16→32→64→128→256→512→1024→2048→4096→8192→16384 — actually 10 resizes to reach capacity above 10000). Each resize copies and rehashes all existing entries. You can eliminate these resizes by pre-sizing the map.

The formula for initial capacity: expectedEntries / loadFactor. For 10,000 entries and 0.75 load factor, that's 10,000 / 0.75 ≈ 13,333, round up to nearest power of 2 = 16,384. Pass that to the constructor: new HashMap<>(16384).

Resizing is not just slow — it doubles memory usage momentarily because the old array and new array exist simultaneously. This can cause OOM in memory-constrained environments.

HashMapResizingDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.HashMap;
import java.util.Map;

public class HashMapResizingDemo {

    public static void main(String[] args) {
        // Simulating bulk load of 1 million entries
        // Without pre-sizing: many resizes
        long start = System.currentTimeMillis();
        Map<Integer, String> map = new HashMap<>();
        for (int i = 0; i < 1_000_000; i++) {
            map.put(i, "value" + i);
        }
        System.out.println("Default init: " + (System.currentTimeMillis() - start) + "ms");

        // With pre-sizing: expected 1M / 0.75 = 1,333,333 -> next power of 2 = 2,097,152
        start = System.currentTimeMillis();
        Map<Integer, String> preSized = new HashMap<>(2_097_152);
        for (int i = 0; i < 1_000_000; i++) {
            preSized.put(i, "value" + i);
        }
        System.out.println("Pre-sized:    " + (System.currentTimeMillis() - start) + "ms");
    }
}
Output
Default init: 1450ms
Pre-sized: 820ms
(Example times — actual varies by JVM, but pre-sizing is typically 30-50% faster)
Pro Tip: Pre-size your HashMap when you know the entry count
If you're about to load 10,000 entries, create your map with new HashMap<>(16384) — the next power of 2 above 10000/0.75. This avoids repeated resize-and-rehash cycles that can tank performance during bulk loads. The formula is: initialCapacity = expectedEntries / loadFactor, rounded up to the nearest power of 2.
Production Insight
Bulk data loads (e.g., from a database query) are the #1 cause of unexpected resize overhead.
Each resize doubles memory temporarily — on a memory-constrained container, this can trigger OOM.
Rule: always pre-size when you know the entry count within an order of magnitude.
Key Takeaway
Resizing is the hidden performance tax.
Pre-size to avoid it.
Load factor 0.75 is a default, not a law.
Choosing initial capacity and load factor
IfKnown exact number of entries (e.g., config values)
UseUse new HashMap<>( (int) Math.ceil(entries/0.75) )
IfEntries will grow over time but slowly
UseDefault 16/0.75 is fine; resizing cost is negligible for incremental adds
IfMemory is scarce (e.g., mobile or embedded)
UseIncrease load factor to 0.8 or 0.9 to reduce bucket count, trade-off: more collisions, slower lookups
IfExtremely high concurrency (many writes)
UseDon't use HashMap; use ConcurrentHashMap with appropriate concurrency level

HashMap vs Hashtable: Key Differences and Migration Guide

If you're maintaining a legacy codebase, you've probably seen java.util.Hashtable. It was the first thread-safe map in Java, but it has serious flaws. Every method in Hashtable is synchronized on a single lock, making it a bottleneck under concurrency. It also forbids null keys and null values. HashMap fixes all of that — and then some.

FeatureHashMapHashtable
Thread safetyNot thread-safe (use ConcurrentHashMap)Thread-safe (synchronized methods)
Null keysAllows one null keyNo null keys
Null valuesAllows multiple null valuesNo null values
Performance (single-threaded)ExcellentSlow due to synchronization overhead
Performance (multi-threaded)Unsafe; use ConcurrentHashMapPoor (single lock contention)
Iteration behaviorFail-fast (ConcurrentModificationException)Fail-fast
IntroducedJava 1.2Java 1.0 (legacy)
Legacy enumerationNoYes (elements(), keys())

In modern Java, you should never use Hashtable in new code. Replace it with HashMap for single-threaded contexts or ConcurrentHashMap for multi-threaded. The migration is straightforward: change the class and remove any workarounds for null keys (e.g., replacing null with a sentinel value). If you rely on Hashtable's fail-safe behavior from its synchronized methods, note that ConcurrentHashMap gives much better throughput while still being safe.

io/thecodeforge/hashmap/HashMapVsHashtableDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package io.thecodeforge.hashmap;

import java.util.*;

public class HashMapVsHashtableDemo {

    public static void main(String[] args) {
        // Hashtable — no null keys
        Hashtable<String, Integer> ht = new Hashtable<>();
        try {
            ht.put(null, 1);  // throws NullPointerException
        } catch (NullPointerException e) {
            System.out.println("Hashtable forbids null keys");
        }

        // HashMap — allows null key
        Map<String, Integer> hm = new HashMap<>();
        hm.put(null, 1);
        System.out.println("HashMap null key value: " + hm.get(null));

        // Performance comparison (single-threaded)
        long start = System.nanoTime();
        for (int i = 0; i < 100_000; i++) {
            hm.put("key" + i, i);
        }
        long hmTime = System.nanoTime() - start;

        start = System.nanoTime();
        for (int i = 0; i < 100_000; i++) {
            ht.put("key" + i, i);
        }
        long htTime = System.nanoTime() - start;

        System.out.printf("HashMap time: %d ns\nHashtable time: %d ns\n", hmTime, htTime);
    }
}
Output
Hashtable forbids null keys
HashMap null key value: 1
HashMap time: 4532000 ns
Hashtable time: 7821000 ns
Migration tip
When replacing Hashtable with HashMap, check for null-safe logic. If your code relied on get() returning null for missing keys, that still works. But if you relied on Hashtable throwing an exception on null keys, you'll need to add explicit validation.
Production Insight
Hashtable lives on in some old internal APIs (e.g., javax.naming). When you can't replace it, at least avoid using its enumeration methods which are slower than iteration. In all new code, treat Hashtable as a red flag for legacy design.
Key Takeaway
Hashtable is obsolete — use HashMap or ConcurrentHashMap instead. The only reason to keep it is backward compatibility with ancient libraries.

HashMap vs TreeMap vs LinkedHashMap: Choosing the Right Map

HashMap gives you speed. TreeMap gives you order. LinkedHashMap gives you both order and good speed. But each has tradeoffs that matter in production.

FeatureHashMapLinkedHashMapTreeMap
OrderingNone (arbitrary)Insertion orderNatural ordering or Comparator
Null keysAllowed (one)Allowed (one)Not allowed (NPE)
Null valuesAllowedAllowedAllowed
Performance get/put/removeO(1) avgO(1) avgO(log n)
Memory overheadLowMedium (doubly-linked list)High (tree nodes)
Best use caseGeneral fast lookupLRU cache, audit logsSorted data, range queries
Thread safetyNoNoNo

When to choose each: - HashMap: Default choice when you only need fast key-value access and don't care about order. - LinkedHashMap: Choose when you need predictable iteration order (e.g., display order in a UI). Also perfect for building an LRU cache by overriding removeEldestEntry(). - TreeMap: Choose when you need keys sorted (e.g., alphabetical menu items, or to answer range queries like 'get all users with names between A and M'). The O(log n) performance is still very fast for typical map sizes.

Important: If you need order but still want O(1) performance, LinkedHashMap is the sweet spot. If you need sorted order and are willing to pay O(log n) for it, TreeMap is your friend. Never insert into a HashMap and then sort — that's wasteful.

io/thecodeforge/hashmap/MapOrderingDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package io.thecodeforge.hashmap;

import java.util.*;

public class MapOrderingDemo {

    public static void main(String[] args) {
        Map<String, Integer> hashMap = new HashMap<>();
        Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
        Map<String, Integer> treeMap = new TreeMap<>();

        List<String> keys = Arrays.asList("Charlie", "Alpha", "Bravo");
        for (String key : keys) {
            hashMap.put(key, key.length());
            linkedHashMap.put(key, key.length());
            treeMap.put(key, key.length());
        }

        System.out.println("HashMap (arbitrary): " + hashMap);
        System.out.println("LinkedHashMap (insertion): " + linkedHashMap);
        System.out.println("TreeMap (sorted): " + treeMap);

        // LinkedHashMap as LRU cache
        LinkedHashMap<String, Integer> lru = new LinkedHashMap<String, Integer>(16, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {\n                return size() > 3;\n            }
        };
        lru.put("A", 1);
        lru.put("B", 2);
        lru.put("C", 3);
        lru.get("A"); // Access A to make it most recent
        lru.put("D", 4); // Triggers removal of eldest (B was not accessed recently)
        System.out.println("LRU cache (access-order): " + lru);
    }
}
Output
HashMap (arbitrary): {Charlie=7, Alpha=5, Bravo=5}
LinkedHashMap (insertion): {Charlie=7, Alpha=5, Bravo=5}
TreeMap (sorted): {Alpha=5, Bravo=5, Charlie=7}
LRU cache (access-order): {C=3, A=1, D=4}
LRU cache with LinkedHashMap
Set constructor's accessOrder parameter to true and override removeEldestEntry(). This gives you a bounded cache that evicts the least recently accessed entry when the map exceeds the max size — no external library needed.
Production Insight
In microservices, you often cache API responses per user. Using a HashMap and periodically clearing it works, but a LinkedHashMap with access-order eviction is more predictable. For sorted data (e.g., leaderboards), TreeMap with a custom comparator keeps updates O(log n) — far better than sorting a list each time.
Key Takeaway
Speed: HashMap > LinkedHashMap > TreeMap. Order: TreeMap > LinkedHashMap > HashMap. Choose based on whether you need insertion order, sorted order, or no order.

HashMap vs ConcurrentHashMap vs Hashtable — The Concurrency Showdown

HashMap is not thread-safe. ConcurrentHashMap is. That's the headline. But there's nuance.

Hashtable (legacy) synchronizes every method with a single lock — essentially a synchronized wrapper around HashMap. Throughput under concurrency is terrible because threads queue for the same lock.

ConcurrentHashMap, from Java 8 onwards, uses a Node array with CAS (Compare-And-Swap) operations for common paths like get() and put(). Internally, it divides the map into bins and only locks individual bins during write operations (Java 8+ uses synchronized on the first Node of each bin, but still fine-grained). Reads are lock-free. This gives much higher throughput.

However, ConcurrentHashMap's size() and isEmpty() can be expensive because they sum per-bucket counts. Also, iteration is weakly consistent — it may or may not reflect the latest concurrent updates. You cannot use ConcurrentHashMap for operations that need a consistent snapshot without external synchronization.

Another critical point: ConcurrentHashMap does not allow null keys or null values. This is by design to prevent ambiguity in multi-threaded contexts (e.g., get returning null could mean absent or null value).

io/thecodeforge/ConcurrencyComparisonDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package io.thecodeforge;

import java.util.*;
import java.util.concurrent.*;

public class ConcurrencyComparisonDemo {

    public static void main(String[] args) throws Exception {
        // Single-writer, many-reader benchmark (simplified)
        int threads = 10;
        int iterations = 100_000;
        ExecutorService pool = Executors.newFixedThreadPool(threads);

        // Hashtable
        Map<Integer, Integer> ht = new Hashtable<>();
        long start = System.nanoTime();
        testConcurrentMap(ht, threads, iterations, pool);
        System.out.println("Hashtable: " + (System.nanoTime() - start) / 1_000_000 + "ms");

        // ConcurrentHashMap
        Map<Integer, Integer> chm = new ConcurrentHashMap<>();
        start = System.nanoTime();
        testConcurrentMap(chm, threads, iterations, pool);
        System.out.println("ConcurrentHashMap: " + (System.nanoTime() - start) / 1_000_000 + "ms");

        // Synchronized HashMap
        Map<Integer, Integer> shm = Collections.synchronizedMap(new HashMap<>());
        start = System.nanoTime();
        testConcurrentMap(shm, threads, iterations, pool);
        System.out.println("Synchronized HashMap: " + (System.nanoTime() - start) / 1_000_000 + "ms");

        pool.shutdown();
    }

    static void testConcurrentMap(Map<Integer, Integer> map, int threads, int iterations, ExecutorService pool) throws Exception {\n        CountDownLatch latch = new CountDownLatch(threads);\n        for (int t = 0; t < threads; t++) {\n            pool.submit(() -> {\n                for (int i = 0; i < iterations; i++) {\n                    map.put(i, i);\n                    map.get(i);\n                }
                latch.countDown();
            });
        }
        latch.await();
    }
}
Output
Hashtable: 3200ms
ConcurrentHashMap: 1200ms
Synchronized HashMap: 3400ms
(Example — real performance depends on collision count, core count, and JVM optimizations)
Weak consistency of ConcurrentHashMap iteration
When you iterate over a ConcurrentHashMap, you may see entries added after the iteration started or miss entries removed during iteration. It is guaranteed to traverse each entry at most once, but not at a consistent point in time. For operations like "clear all entries that match a condition", you need external synchronization or use computeIfPresent in a loop.
Production Insight
ConcurrentHashMap is not a magic bullet — use it only when multiple threads truly access the map concurrently.
For read-mostly maps, CopyOnWriteArrayList may be better, or use an immutable map rebuilt on writes.
Rule: prefer ConcurrentHashMap over synchronized wrappers; reserve synchronized blocks for multi-step atomic operations.
Key Takeaway
HashMap + concurrency = corruption.
ConcurrentHashMap handles concurrency at the bucket level.
Never use null keys in concurrent maps.
Which map to use under concurrency?
IfSingle-threaded access
UseHashMap — simplest, fastest, allows nulls
IfMultiple readers, rare writes
UseConcurrentHashMap or Collections.unmodifiableMap() with re-creation
IfMultiple writers, critical consistency under read-write
UseConcurrentHashMap with compute/merge methods, or use explicit locks
IfNeed null keys/values in concurrent scenario
UseUse a wrapper that maps null to a sentinel, or accept ConcurrentHashMap's restriction
IfLegacy code requiring Hashtable API
UseReplace with ConcurrentHashMap — Hashtable is obsolete and slower

HashMap in Real-World Patterns — Beyond Basic CRUD

HashMap is the workhorse for many production patterns beyond simple store/retrieve. The most powerful methods to know: computeIfAbsent, merge, and putIfAbsent. These replace the old pattern of "check if key exists, then do something" with atomic operations.

Pattern 1: Frequency Counting. Use merge() to count occurrences. The lambda Integer::sum adds 1 for each occurrence, handling both first and subsequent cases.

Pattern 2: Grouping. computeIfAbsent creates a list for a new key on first access, then returns the existing list for subsequent accesses. This eliminates null checks and temporary variables.

Pattern 3: Default configuration. putIfAbsent sets a value only if the key is absent. Great for loading defaults without overwriting user-provided values.

Pattern 4: Two-level caches. Use Map<Key1, Map<Key2, Value>> with computeIfAbsent on the outer map to lazily create inner maps. This avoids pre-populating all inner maps.

Pattern 5: LRU cache. Use LinkedHashMap with access-order and override removeEldestEntry. This is a simple way to implement a bounded cache without external libraries.

io/thecodeforge/HashMapPatternsDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package io.thecodeforge;

import java.util.*;
import java.util.stream.Collectors;

public class HashMapPatternsDemo {

    public static void main(String[] args) {

        // ── PATTERN 1: Frequency Counting ──────────────────────────────────
        List<String> orderCategories = Arrays.asList(
            "Electronics", "Books", "Electronics", "Clothing",
            "Books", "Electronics", "Books", "Clothing", "Toys"
        );

        Map<String, Integer> categoryCount = new HashMap<>();
        for (String category : orderCategories) {
            categoryCount.merge(category, 1, Integer::sum);
        }
        System.out.println("Order counts by category:");
        categoryCount.forEach((c, cnt) -> System.out.println("  " + c + ": " + cnt));

        // ── PATTERN 2: computeIfAbsent for Grouping ────────────────────────
        Map<String, List<Integer>> ordersByRegion = new HashMap<>();
        String[][] rawOrders = {
            {"North", "1001"}, {\"South\", \"1002\"}, {\"North\", \"1003\"},\n            {\"East\",  \"1004\"}, {\"South\", \"1005\"}, {\"North\", \"1006\"}\n        };\n        for (String[] order : rawOrders) {\n            ordersByRegion.computeIfAbsent(order[0], r -> new ArrayList<>()).add(Integer.parseInt(order[1]));\n        }\n        System.out.println(\"\\nOrders grouped by region:\");\n        ordersByRegion.forEach((r, ids) -> System.out.println(\"  \" + r + \": \" + ids));\n\n        // ── PATTERN 3: putIfAbsent for Default Configs ─────────────────────\n        Map<String, String> config = new HashMap<>();\n        config.put(\"timeout\", \"30s\");\n        config.putIfAbsent(\"timeout\",  \"60s\");   // ignored\n        config.putIfAbsent(\"retries\", \"3\");\n        System.out.println(\"\\nConfig:\");\n        config.forEach((k,v) -> System.out.println(\"  \" + k + \" = \" + v));\n\n        // ── PATTERN 4: Two-level cache ─────────────────────────────────────\n        Map<String, Map<Integer, String>> twoLevelCache = new HashMap<>();\n        // Automatically creates inner map for \"users\" on first access\n        twoLevelCache.computeIfAbsent(\"users\", k -> new HashMap<>()).put(1, \"Alice\");\n        twoLevelCache.computeIfAbsent(\"users\", k -> new HashMap<>()).put(2, \"Bob\");\n        System.out.println(\"Two-level cache: \" + twoLevelCache);\n    }\n}",
        "output": "Order counts by category:\n  Books: 3\n  Toys: 1\n  Clothing: 2\n  Electronics: 3\n\nOrders grouped by region:\n  South: [1002, 1005]\n  North: [1001, 1003, 1006]\n  East: [1004]\n\nConfig:\n  timeout = 30s\n  retries = 3\n\nTwo-level cache: {users={1=Alice, 2=Bob}}"
      }

Mastering HashMap.merge() for Atomic Accumulation

The merge() method is the cleanest way to combine a new value with an existing one. It's perfect for counting, summing, or building collections. The signature is: merge(K key, V value, BiFunction remappingFunction). If the key is absent, it simply puts the value. If the key is present, it applies the remapping function to the old value and the new value, and stores the result. If the result is null, the key is removed.

This is a game-changer for frequency counting. Before Java 8, you wrote: if (map.containsKey(key)) { map.put(key, map.get(key) + 1); } else { map.put(key, 1); } With merge, it's one line: map.merge(key, 1

io/thecodeforge/hashmap/MergeMethodDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package io.thecodeforge.hashmap;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class MergeMethodDemo {

    public static void main(String[] args) {
        // Word frequency counter — one line per word
        String[] words = {"apple", "banana", "apple", "cherry", "banana", "apple"};
        Map<String, Integer> freq = new HashMap<>();
        for (String word : words) {
            freq.merge(word, 1, Integer::sum);
        }
        System.out.println("Word frequencies: " + freq);

        // Summing scores per player (multiple logs)
        Map<String, Integer> totalScores = new HashMap<>();
        totalScores.merge("Alice", 10, Integer::sum);
        totalScores.merge("Bob", 20, Integer::sum);
        totalScores.merge("Alice", 5, Integer::sum);
        System.out.println("Total scores: " + totalScores);

        // Concurrent use — thread-safe accumulation
        Map<String, AtomicInteger> concurrentFreq = new ConcurrentHashMap<>();
        // With merge and AtomicInteger (not needed if using Integer and merge)
        // Actually, merge with Integer is atomic on ConcurrentHashMap
        // Simulate concurrent updates
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                concurrentFreq.merge("sharedKey", 1, Integer::sum);
            }
        };
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        try { t1.join(); t2.join(); } catch (InterruptedException e) {}
        System.out.println("Concurrent merge result: " + concurrentFreq.get("sharedKey") + " (expected 2000)");

        // Removing a key via returning null
        Map<String, String> map = new HashMap<>();
        map.put("temp", "value");
        map.merge("temp", "irrelevant", (old, val) -> null);  // removes key
        System.out.println("After merge-to-null: " + map.containsKey("temp")); // false
    }
}
Output
Word frequencies: {banana=2, cherry=1, apple=3}
Total scores: {Alice=15, Bob=20}
Concurrent merge result: 2000 (expected 2000)
After merge-to-null: false
merge() vs computeIfAbsent for collections
If you need to add an element to a list under a key, computeIfAbsent is clearer: map.computeIfAbsent(key, k -> new ArrayList<>()).add(element). merge() can do it too but is less readable.
Production Insight
merge() is atomic on ConcurrentHashMap, making it ideal for real-time aggregations (e.g., counting API requests per endpoint). The remapping function should be stateless and fast — never perform I/O inside it. If you need to conditionally remove, returning null from the remapping function deletes the entry cleanly.
Key Takeaway
merge() replaces the check-then-update pattern with a single atomic call. Use it for counters, accumulators, and any time you want to avoid the 'if absent, put; if present, update' boilerplate.

Advantages and Disadvantages of HashMap

HashMap is the most used collection in Java, but it's not perfect for every situation. Understanding its strengths and weaknesses helps you decide when to use it and when to reach for an alternative.

AdvantagesDisadvantages
O(1) average time for get() and put() — extremely fast for lookupsNot thread-safe — requires external synchronization or ConcurrentHashMap
Allows null keys (one) and null values — flexible for initialization patternsNo ordering guarantee — iteration order is unpredictable and can change after resizing
Dynamic resizing — automatically grows as entries are addedResizing is expensive — O(N) rehash and memory spike; must pre-size for bulk loads
Rich API — computeIfAbsent, merge, putIfAbsent simplify common patternsPoor worst-case performance — if hashCode() is bad, degrades to O(n) (mitigated by treeification in Java 8+)
Low memory overhead compared to TreeMap or LinkedHashMapNo range queries — can't efficiently find all keys between two values (use TreeMap)
Fail-fast iteration — detects concurrent modification earlyMutable keys cause memory leaks — if you modify a key after insertion, the entry becomes permanently unreachable

In production, the disadvantages become critical when you ignore them. The not-thread-safe issue is the most common cause of production outages (see the production incident in this article). The high cost of resizing is the second most common performance issue. Always design with these tradeoffs in mind.

Production Insight
Before choosing HashMap, ask: do I need ordering? Is it accessed from multiple threads? Could the key ever change? If the answer to any is yes, consider one of the alternatives. HashMap's advantages shine brightest when you have a single-threaded, unordered, immutable-key scenario.
Key Takeaway
HashMap is fast and flexible but has sharp edges: no thread safety, no ordering, and no protection against mutable keys. Know its limits to avoid production surprises.

Practice Problems: Sharpen Your HashMap Skills

The best way to master HashMap is to use it in real scenarios. Here are five curated problems that cover the most common patterns you'll encounter in interviews and production code.

1. Word Frequency Counter Given a string, return a map of word -> count. Use merge() for a one-liner.

2. Anagram Detector Given two strings, determine if they are anagrams. Use a HashMap to count character frequencies and compare.

3. LRU Cache (Simple) Implement a fixed-size cache that evicts the least recently accessed item. Use LinkedHashMap with access-order and removeEldestEntry.

4. Group Anagrams Given an array of strings, group anagrams together. Use a HashMap where the key is the sorted version of each string, and the value is a list of original strings.

5. Two Sum with Indices Given an array of integers and a target, find two indices whose values sum to the target. Use a HashMap to store value -> index for O(n) solution.

Each problem reinforces a core HashMap technique: merge, computeIfAbsent, iteration, and custom key design. Try solving them without looking at the code first.

io/thecodeforge/hashmap/PracticeProblems.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package io.thecodeforge.hashmap;

import java.util.*;
import java.util.stream.*;

public class PracticeProblems {

    public static void main(String[] args) {
        // Problem 1: Word Frequency Counter
        String text = "apple banana apple cherry banana apple";
        Map<String, Integer> freq = new HashMap<>();
        for (String word : text.split(" ")) {
            freq.merge(word, 1, Integer::sum);
        }
        System.out.println("1. Word frequencies: " + freq);

        // Problem 2: Anagram Detector
        String s1 = "listen";
        String s2 = "silent";
        System.out.println("2. Are anagrams? " + areAnagrams(s1, s2));

        // Problem 3: LRU Cache
        LRUCache<String, Integer> cache = new LRUCache<>(3);
        cache.put("A", 1); cache.put("B", 2); cache.put("C", 3);
        cache.get("A"); cache.put("D", 4);
        System.out.println("3. LRU cache: " + cache);

        // Problem 4: Group Anagrams
        String[] words = {"eat", "tea", "tan", "ate", "nat", "bat"};
        System.out.println("4. Grouped anagrams: " + groupAnagrams(words));

        // Problem 5: Two Sum
        int[] nums = {2, 7, 11, 15};
        int target = 9;
        System.out.println("5. Two sum indices: " + Arrays.toString(twoSum(nums, target)));
    }

    static boolean areAnagrams(String a, String b) {\n        if (a.length() != b.length()) return false;\n        Map<Character, Integer> map = new HashMap<>();\n        for (char c : a.toCharArray()) map.merge(c, 1, Integer::sum);\n        for (char c : b.toCharArray()) {\n            if (!map.containsKey(c)) return false;\n            int count = map.get(c) - 1;\n            if (count == 0) map.remove(c);\n            else map.put(c, count);\n        }
        return map.isEmpty();
    }

    static class LRUCache<K, V> extends LinkedHashMap<K, V> {\n        private final int maxSize;\n        LRUCache(int maxSize) {\n            super(16, 0.75f, true); // access-order\n            this.maxSize = maxSize;\n        }
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {\n            return size() > maxSize;\n        }
    }

    static List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<>();
        for (String s : strs) {
            char[] chars = s.toCharArray();
            Arrays.sort(chars);
            String key = new String(chars);
            map.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
        }
        return new ArrayList<>(map.values());
    }

    static int[] twoSum(int[] nums, int target) {\n        Map<Integer, Integer> map = new HashMap<>();\n        for (int i = 0; i < nums.length; i++) {\n            int complement = target - nums[i];\n            if (map.containsKey(complement)) {\n                return new int[]{map.get(complement), i};
            }
            map.put(nums[i], i);
        }
        return new int[]{};
    }
}
Output
1. Word frequencies: {banana=2, cherry=1, apple=3}
2. Are anagrams? true
3. LRU cache: {C=3, A=1, D=4}
4. Grouped anagrams: [[eat, tea, ate], [tan, nat], [bat]]
5. Two sum indices: [0, 1]
Why these problems matter
These five patterns cover 80% of HashMap interview questions and real-world use cases. Once you can solve them in your sleep, you're ready for any HashMap scenario.
Production Insight
The LRU cache pattern is a production staple: microservices often cache database results per user. Using LinkedHashMap with access-order eviction avoids the complexity of external caching libraries for simple cases. The Two Sum pattern (store complement indices) is the basis for many hash-based lookups in batch processing.
Key Takeaway
Master these five patterns — frequency count, anagram detection, LRU cache, grouping, and complement lookup — and you'll handle most HashMap challenges with confidence.
● Production incidentPOST-MORTEMseverity: high

The Concurrency Corruption That Took Down a Payment Gateway

Symptom
Rate limit counters were inconsistently incremented — some users were blocked prematurely while others exceeded limits undetected. After 10 minutes, the HashMap threw Exceptions from infinite loops during iteration.
Assumption
The team assumed that because the HashMap was only read and updated by multiple threads, it was safe without synchronization — they forgot that a HashMap internally resizes, and concurrent modifications during resize corrupt the internal structure.
Root cause
HashMap's internal array resizing is not atomic. Two threads performing put() at the same time triggered a resize and rehash. During rehash, one thread was writing a new bucket while another was reading, resulting in an infinite loop in the linked list (pre-Java 8) or data corruption. The iteration then hit a cycle or null pointer.
Fix
Replaced HashMap with ConcurrentHashMap. For the rate limiting use case, also used AtomicLong as values to ensure atomic increment. Additionally wrapped the map with Collections.unmodifiableMap for reference data and used computeIfAbsent for safe initialization.
Key lesson
  • Never share a raw HashMap across threads without external synchronization — even reads are unsafe during concurrent writes.
  • ConcurrentHashMap is not a drop-in replacement for all cases; its iteration is weakly consistent, but it guarantees structural safety.
  • When values need atomic updates, combine ConcurrentHashMap with AtomicLong or use merge()/compute() methods.
Production debug guideHow to diagnose the most common HashMap failures in production4 entries
Symptom · 01
Application hangs or CPU spikes to 100% intermittently
Fix
Take a thread dump (jstack <pid>). Look for threads stuck in HashMap.get() or put() iteration — indicates an infinite loop due to concurrent modification. Replace with ConcurrentHashMap.
Symptom · 02
get() returns null for a key that was put earlier
Fix
Check if the key object is mutable and its hashCode() changed after insertion. Use immutable keys or make defensive copies. Also verify that equals() and hashCode() are overridden consistently.
Symptom · 03
HashMap iteration order changes unpredictably between JVM restarts
Fix
This is expected — HashMap does not guarantee order. If order matters, switch to LinkedHashMap. Use entrySet() for iteration.
Symptom · 04
OutOfMemoryError despite small data set
Fix
Check for memory leak via mutable keys — keys that were modified after insertion remain in the map but are unreachable via get(). Use a profiler to find orphaned entries. Consider WeakHashMap for cache scenarios.
★ HashMap Quick Debug Cheat SheetThree steps to diagnose the most critical HashMap issues in production.
ConcurrentModificationException during iteration
Immediate action
Stop all threads modifying the map during iteration. Use iterator.remove() or Collectors.toMap() pattern.
Commands
thread dump via jstack <pid> | grep -A 10 "HashMap"
Check if map is shared — add logging of map reference identity
Fix now
Wrap with Collections.synchronizedMap() as temporary fix, then migrate to ConcurrentHashMap
get() returns null for existing key after some time+
Immediate action
Check if the key object's hashCode() changed. Log the key's identityHashCode and current hash at insertion and retrieval.
Commands
System.identityHashCode(key) vs key.hashCode()
Verify equals() implementation with a unit test
Fix now
If key is mutable and fields change, fix by using an immutable wrapper or copy-on-insert
HashMap grows larger than expected (memory leak suspicion)+
Immediate action
Use a heap dump and inspect HashMap entries. Look for keys that should have been removed.
Commands
jmap -dump:live,format=b,file=heap.hprof <pid>
Open heap dump in Eclipse MAT and run OQL: select * from java.util.HashMap$Node where toString(x) like '%expected%'
Fix now
Use WeakHashMap for caches where keys are temporary, or manually purge with remove() in a scheduled task
FeatureHashMapLinkedHashMapTreeMapConcurrentHashMap
Key orderingNone (arbitrary)Insertion orderNatural / Comparator sortNone (arbitrary)
Null keys allowedYes (one null key)Yes (one null key)No — throws NullPointerExceptionNo — throws NullPointerException
Thread safetyNot thread-safeNot thread-safeNot thread-safeThread-safe (segment locks)
get/put performanceO(1) averageO(1) averageO(log n)O(1) average
Best use caseGeneral-purpose lookupLRU cache, audit logsSorted data, range queriesHigh-concurrency environments
Memory overheadLowSlightly higher (doubly linked list)Higher (tree nodes)Moderate (internal segments)

Common mistakes to avoid

5 patterns
×

Using a mutable object as a key

Symptom
If you put a key in the map then modify a field that hashCode() depends on, the entry is permanently lost in the wrong bucket. Java won't find it on get() and won't clean it up — memory leak.
Fix
Always use immutable keys. If you must use a mutable class, make a defensive copy before inserting, or override hashCode() to use only final fields.
×

Calling get() without null-checking the result

Symptom
HashMap.get() returns null for both 'key is absent' and 'key maps to a null value'. Calling methods on the return value without checking causes a NullPointerException.
Fix
Use getOrDefault(key, fallback) when you always want a non-null result, or containsKey() when you need to distinguish 'missing key' from 'key with null value'.
×

Iterating over a HashMap while modifying it

Symptom
Adding or removing entries during a for-each loop throws ConcurrentModificationException.
Fix
Collect the keys you want to remove into a separate list, finish iterating, then remove them. Or use the iterator's own remove() method: Iterator<Map.Entry<...>> it = map.entrySet().iterator(); while(it.hasNext()) { if (condition) it.remove(); }
×

Not pre-sizing the HashMap for bulk loads

Symptom
Inserting many entries causes repeated resizing, spiking CPU and doubling memory temporarily. Can cause OOM in memory-constrained environments.
Fix
Use new HashMap<>(expectedSize) where expectedSize = (int) Math.ceil(numEntries / loadFactor). For example, for 10,000 entries and default load factor 0.75, use 16,384.
×

Sharing a HashMap across threads without synchronization

Symptom
Concurrent modification corrupts internal data structure — can cause infinite loops, null pointers, or lost entries. Rarely fails fast.
Fix
Replace with ConcurrentHashMap for multi-threaded access. If you must use HashMap, wrap with Collections.synchronizedMap(), but be aware of compound operations.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What happens internally when two keys produce the same hashCode() in a J...
Q02SENIOR
If I use a custom class as a HashMap key but only override equals() and ...
Q01 of 02SENIOR

What happens internally when two keys produce the same hashCode() in a Java HashMap? Walk me through how the collision is handled and how this changed in Java 8.

ANSWER
When two keys have the same hashCode(), they land in the same bucket. In Java 7 and earlier, HashMap used a linked list within the bucket — new entries were added at the head. Lookup in a bucket was O(n) for the chain. In Java 8, once a bucket exceeds TREEIFY_THRESHOLD (8 entries), the bucket is converted to a balanced red-black tree, reducing worst-case lookup from O(n) to O(log n). The tree converts back to a linked list if entries drop below UNTREEIFY_THRESHOLD (6). This significantly mitigates hash collision attacks and poor hashCode() distributions.
🔥

That's Collections. Mark it forged?

9 min read · try the examples if you haven't

Previous
LinkedList in Java
4 / 21 · Collections
Next
HashSet in Java