Junior 4 min · March 06, 2026

HashMap O(n) Bug — Java Collections Interview Questions

95% CPU spike from a HashMap O(n) bug? Fix it by overriding equals() and hashCode().

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • The Collections Framework provides reusable data structures: List, Set, Queue, and Map.
  • Map is NOT a Collection — it has its own hierarchy with put() not add().
  • ArrayList is cache-friendly; LinkedList is almost never the right choice in production.
  • Fail-fast iterators throw ConcurrentModificationException on structural modification.
  • HashMap degrades to O(n) with poor hashCode; Java 8 uses tree bins at threshold 8.
  • Interviewers test trade-off awareness, not just API knowledge.
Plain-English First

Imagine you're organising a music festival. You need a guest list (no duplicates), a queue of performers waiting to go on stage (order matters), and a lookup table mapping wristband colours to backstage areas. Java Collections are exactly those organisational tools — List, Queue, and Map — built into the language so you don't have to reinvent them every project. The Collections Framework is just Java's pre-built toolkit of smart containers, each one optimised for a specific job.

Every Java backend role — from fintech startups to FAANG-scale companies — will grill you on Collections. Not because interviewers enjoy trivia, but because how you choose and use data structures reveals whether you actually understand the trade-offs of your code. A developer who reaches for an ArrayList when they need a HashSet is a developer who will accidentally write O(n) lookups in production hot paths.

The Collections Hierarchy — Why It Exists and How It Flows

The Java Collections Framework (JCF) was introduced in Java 1.2 to replace a mess of unrelated classes — Vector, Hashtable, Stack — that had no common interface and couldn't be swapped out without rewriting calling code. The designers solved this with a clean interface hierarchy.

At the top sits Iterable, which just means 'you can loop over me'. Below it is Collection, which adds size(), add(), remove(), and contains(). From Collection, three main branches split off: List (ordered, index-based), Set (no duplicates), and Queue (designed for hold-and-process workflows). Map sits separately because it stores key-value pairs rather than individual elements — it's not technically a Collection, which catches a lot of people out in interviews.

Understanding WHY the hierarchy is designed this way lets you write code to interfaces (List instead of ArrayList), making it trivially easy to swap implementations later without breaking callers. That's the entire point of the abstraction.

io.thecodeforge.collections.HierarchyDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package io.thecodeforge.collections;

import java.util.*;

public class HierarchyDemo {
    public static void main(String[] args) {
        // Programming to the interface (List) is the golden rule.
        List<String> festivalLineup = new ArrayList<>();
        festivalLineup.add("Arctic Monkeys");
        festivalLineup.add("Kendrick Lamar");
        festivalLineup.add("Arctic Monkeys"); // Duplicates allowed

        // Sets enforce uniqueness at the structural level.
        Set<String> uniqueArtists = new HashSet<>(festivalLineup);

        // Map is an outlier — it doesn't extend the Collection interface.
        Map<String, String> wristbandAccess = new HashMap<>();
        wristbandAccess.put("RED", "Backstage");

        boolean isCollection = wristbandAccess instanceof Collection;
        System.out.println("Is HashMap a Collection? " + isCollection);
    }
}
Output
Is HashMap a Collection? false
Watch Out: Map Is NOT a Collection
Every year, candidates confidently say 'Map extends Collection'. It doesn't. Map has its own hierarchy. Say this clearly in an interview and you'll immediately stand out. The tell: Map has no add() method — it has put() instead.
Production Insight
In a production codebase, programming to interfaces prevents cascading changes when you swap implementations.
A real refactor from ArrayList to CopyOnWriteArrayList for thread safety took 5 minutes instead of 2 days because the code was written to List.
Rule: always declare variables as the most general interface that provides what you need.
Key Takeaway
Collection interface branches: List, Set, Queue.
Map is separate — not a Collection.
Program to interfaces, not implementations.
This abstraction is the single most powerful design decision in JCF.

ArrayList vs LinkedList vs HashMap — Choosing the Right Tool

The single most common Collections interview question is: 'When would you use ArrayList over LinkedList?' Most candidates recite 'ArrayList is fast for random access, LinkedList is fast for insertion'. That answer is technically correct but dangerously incomplete.

In practice, LinkedList is almost never the right choice. Its nodes are scattered across heap memory, so traversal hammers the CPU cache — ArrayList's contiguous memory block is cache-friendly and wins in benchmarks even for middle-of-list insertions once lists exceed a few hundred elements. The real alternative to ArrayList for frequent insertions is ArrayDeque or a different algorithm entirely.

HashMap is the workhorse for O(1) average get/put. But 'average' hides a secret: if your keys have poor hashCode() implementations, HashMap degrades to O(n) because all keys land in the same bucket. Java 8 fixed the worst case by converting buckets into balanced trees (O(log n)) when a bucket exceeds 8 entries — but the best fix is always writing good hashCode() methods.

io.thecodeforge.collections.LoadFactorDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package io.thecodeforge.collections;

import java.util.HashMap;
import java.util.Map;

public class LoadFactorDemo {
    public static void main(String[] args) {
        // Production tip: If you know you need to store 1000 items,
        // initialize with capacity to avoid expensive resizing (rehashing).
        // formula: initialCapacity = expectedSize / loadFactor + 1
        int expectedSize = 1000;
        Map<Integer, String> optimizedMap = new HashMap<>((int) (expectedSize / 0.75f) + 1);

        for (int i = 0; i < expectedSize; i++) {
            optimizedMap.put(i, "Value " + i);
        }
        System.out.println("Map size: " + optimizedMap.size());
    }
}
Output
Map size: 1000
Interview Gold: The LinkedList Trap
If an interviewer asks 'when would you use LinkedList?', the honest answer is: 'Almost never in production Java. I'd use ArrayDeque for queue/deque operations, which is faster and more memory-efficient.' That kind of nuanced answer signals real-world experience, not textbook parroting.
Production Insight
LinkedList's memory overhead per element is ~24 bytes extra due to prev/next pointers.
ArrayList reserve with initial capacity avoids multiple resize copies — each resize costs O(n) and temporarily doubles memory.
HashMap default load factor 0.75 is a time-space trade-off: lower load factor wastes memory, higher increases collisions.
Rule: initialise collections with expected size when known.
Key Takeaway
Avoid LinkedList in production — prefer ArrayList or ArrayDeque.
HashMap performance depends on hashCode quality.
Always pre-size collections when the expected size is known.
Trade-off awareness beats rote memorisation in interviews.

Fail-Fast vs Fail-Safe Iterators — The Concurrency Trap Everyone Falls Into

Here's a scenario that trips up mid-level developers all the time: you're iterating over a List and removing elements that match a condition. You write a for-each loop, call list.remove() inside it, and boom — ConcurrentModificationException. This isn't a threading bug. It happens on a single thread. Why?

ArrayList's iterator is fail-fast. It tracks a modCount counter that increments on every structural modification. When the iterator's next() checks its expected count against the list's current count and finds a mismatch, it throws immediately — not silently corrupt data. This is a deliberate design choice: fail loudly rather than return unpredictable results.

The fix is to use Iterator.remove() directly, or the removeIf() method (Java 8+), or collect elements to remove into a separate list first.

io.thecodeforge.collections.SafeRemoval.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package io.thecodeforge.collections;

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

public class SafeRemoval {
    public static void main(String[] args) {
        List<String> logs = new ArrayList<>(List.of("INFO", "DEBUG", "ERROR", "WARN"));

        // The modern, production-standard way to remove items safely
        logs.removeIf(log -> log.equals("DEBUG"));

        // Alternative: Using streams to create a new immutable collection
        List<String> criticalLogs = logs.stream()
            .filter(log -> !log.equals("INFO"))
            .collect(Collectors.toUnmodifiableList());

        System.out.println("Cleaned Logs: " + criticalLogs);
    }
}
Output
Cleaned Logs: [ERROR, WARN]
Pro Tip: removeIf() Is Your Default
In modern Java (8+), removeIf() is the cleanest way to filter-remove from a List. It's readable, safe, and handles the iterator mechanics for you. Reserve CopyOnWriteArrayList for genuinely concurrent scenarios — its write cost is O(n) every time.
Production Insight
In production, a single-thread ConcurrentModificationException is often caused by a lambda inside a forEach that calls add/remove on the same list.
The same modCount check catches multi-thread modification — but there the fix is to use a concurrent collection.
Rule: never modify a collection inside a loop unless using the iterator's own remove() or removeIf().
Key Takeaway
Fail-fast iterators detect structural changes via modCount.
Use Iterator.remove() or removeIf() for safe removal.
For concurrent access, use CopyOnWriteArrayList or ConcurrentHashMap.
Fail-safe iterators (like those on ConcurrentHashMap) work on a snapshot — no exception but possibly stale data.

HashMap Internals — hashCode(), equals(), and Java 8's Tree Buckets

If there's one Collections topic that separates candidates who've read books from candidates who've debugged production systems, it's HashMap internals. You need to know this cold.

When you call map.put(key, value), Java computes key.hashCode(), applies a supplemental hash function to spread bits, then uses (n-1) & hash to find a bucket index. If the bucket is empty, your entry goes in. If it's occupied, Java calls equals() to check if it's the same key (update) or a different key (collision, add to the bucket chain).

Java 8 made a critical improvement: when a bucket chain exceeds 8 entries AND the table has at least 64 buckets, the chain converts to a balanced Red-Black tree. This caps worst-case lookup at O(log n) instead of O(n). The practical lesson: if your key class overrides equals() but NOT hashCode(), all instances will hash to the same bucket, causing HashMap to behave like a linked list. Always override both, always together.

io.thecodeforge.collections.ProperKey.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
package io.thecodeforge.collections;

import java.util.Objects;

/**
 * A production-grade immutable key for use in HashMaps.
 */
public final class ProperKey {
    private final String id;
    private final int version;

    public ProperKey(String id, int version) {
        this.id = id;
        this.version = version;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ProperKey that = (ProperKey) o;
        return version == that.version && Objects.equals(id, that.id);
    }

    @Override
    public int hashCode() {
        // Uses a high-quality hash multiplier (31) internally
        return Objects.hash(id, version);
    }
}
Output
Class compiled. Reliable for use in hashing-based collections.
Watch Out: The Mutable Key Disaster
Never use a mutable object as a HashMap key. If you put() an entry then modify the key object's fields, its hashCode changes, and HashMap looks in the wrong bucket — your entry becomes permanently unreachable even though it's still in the map. This is one of the sneakiest memory leaks in Java.
Production Insight
A common production bug is using java.awt.Point as a key — point objects get reused and mutated.
Tree conversion threshold 8 is based on empirical Poisson distribution; at 0.75 load factor, a bucket with 8 entries has probability ~0.00000006.
If your HashMap is frequently resizing, you're paying O(n) copying cost — always set initial capacity to (expectedSize/0.75)+1.
Rule: use immutable keys (records, Strings, Integers) to avoid hash surprises.
Key Takeaway
HashMap uses hashCode->bucket, then equals for collision resolution.
Always override equals AND hashCode together.
Use immutable keys to prevent lost entries.
Java 8 treeifies buckets at threshold 8 to protect against hash collision attacks.
Initial capacity prevents expensive resizing.

ConcurrentHashMap: The Thread-Safe HashMap Every Senior Engineer Must Understand

When interviewers ask 'How would you handle concurrent access to a Map?', the correct answer is never Hashtable. Hashtable is legacy — it adds synchronized to every method, serialising all access, killing throughput. The modern answer is ConcurrentHashMap.

ConcurrentHashMap in Java 8+ uses a completely different approach: it partitions the map into a set of buckets (the same as HashMap) but uses Compare-And-Swap (CAS) for lock-free reads, and synchronizes only on the first node of a bucket for writes. This allows multiple threads to read and write different buckets simultaneously. The internal structure is a Node array where each node is either a linked list node or a tree node (same as HashMap).

Key interview points: ConcurrentHashMap does NOT lock the entire map for reads. It uses volatile reads on the table reference and per-bucket CAS for insertion. The size() method is not a constant-time operation in Java 8+ — it requires summing per-bucket counts. Also, ConcurrentHashMap does not allow null keys or null values (to avoid ambiguity between 'key missing' and 'key mapped to null').

io.thecodeforge.collections.ConcurrentHashMapDemo.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
package io.thecodeforge.collections;

import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

public class ConcurrentHashMapDemo {
    public static void main(String[] args) {
        Map<String, Integer> hitCount = new ConcurrentHashMap<>();

        // Thread-safe, lock-free read path
        // Multiple threads can call merge() concurrently without corruption
        for (int i = 0; i < 10; i++) {
            int threadId = i;
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    hitCount.merge("/api/checkout", 1, Integer::sum);
                }
            }).start();
        }

        // Let threads finish (naive join, better use CountDownLatch in production)
        try { Thread.sleep(2000); } catch (InterruptedException e) { }

        System.out.println("Final count: " + hitCount.get("/api/checkout"));
    }
}
Output
Final count: 10000
Interview Secret: No Nulls in ConcurrentHashMap
ConcurrentHashMap disallows null keys and null values. The designers made this choice because map.get(key) returning null is ambiguous. It could mean the key is not present, or the key is mapped to null. In a concurrent context, distinguishing these cases requires additional locking. So they ban nulls entirely. Mention this in an interview — it shows you've read the source.
Production Insight
In high-throughput systems, using ConcurrentHashMap for caching can double throughput compared to synchronized HashMap.
However, the computeIfAbsent() method is not always atomic in older versions — check JVM version; in Java 9+ it was fixed.
Rule: always use ConcurrentHashMap for shared mutable maps in multi-threaded code. Never use Hashtable.
Key Takeaway
ConcurrentHashMap uses CAS for reads, bucket-level locking for writes.
No null keys or values allowed.
Performance scales with number of buckets, not global locks.
Prefer merge() and computeIfAbsent() over put() for atomic updates.
Remember: modCount-based fail-fast iterators are not used — iterators are weakly consistent.
● Production incidentPOST-MORTEMseverity: high

Hashmap Degradation in a High-Throughput API

Symptom
Response times jumped from 50ms to 8s within 30 minutes of deployment. CPU usage hit 95% on all instances.
Assumption
The new feature used a HashMap with a custom key. The team assumed performance would be fine because they'd seen HashMaps work fast before.
Root cause
The key class overrode equals() but not hashCode(). All keys hashed to bucket 0, turning the HashMap into a linked list. With 500k entries, every get() was O(n) instead of O(1).
Fix
Implemented proper hashCode() using Objects.hash(id, version). Also added a unit test that validates hash distribution for at least 10k keys.
Key lesson
  • Always override both equals() and hashCode() for any class used as a Map key.
  • Add integration tests that measure performance under load before deploying HashMap-dependent code.
  • Use immutable keys to prevent the mutable-key disaster (where a key's hash changes after insertion).
Production debug guideHow to identify and fix fail-fast iterator violations on single and multiple threads3 entries
Symptom · 01
ConcurrentModificationException thrown inside a for-each loop, single thread
Fix
Check for list.add() or list.remove() called inside the loop. Replace with Iterator.remove() or Collection.removeIf().
Symptom · 02
ConcurrentModificationException in multi-threaded code (one thread writes, another reads)
Fix
Use CopyOnWriteArrayList or ConcurrentHashMap. For existing code, wrap collection with Collections.synchronizedList() and synchronize on it during iteration.
Symptom · 03
Exception stack trace points to iterator.next() but modCount seems unchanged
Fix
Look for nested iteration over same collection by different threads. The modCount is per-collection — any thread can modify and cause exception in iterating threads.
★ Collections Diagnostic Cheat SheetQuick commands and patterns to diagnose performance and concurrency issues with Java Collections
HashMap performance degradation under load
Immediate action
Capture a heap dump and analyse key distribution using Eclipse MAT or jmap -histo:live
Commands
jcmd <pid> GC.heap_dump /tmp/dump.hprof
Use Eclipse MAT OQL: SELECT * FROM java.util.HashMap$Node WHERE bucketIndex = 0
Fix now
Fix the hashCode() implementation in the key class and rebuild the map with proper capacity.
ConcurrentModificationException in production logs+
Immediate action
Check the stack trace to identify which collection is being modified during iteration
Commands
grep -r 'ConcurrentModificationException' /var/log/app/*.log
Use jstack to capture thread dump and see which threads hold the collection reference
Fix now
Replace the collection with a thread-safe alternative (CopyOnWriteArrayList, ConcurrentHashMap) or add external synchronization.
Memory leak due to unbounded HashMap growth+
Immediate action
Check if HashMap is used as a cache without eviction policy
Commands
jcmd <pid> GC.class_histogram | head -20
Monitor heap usage: jstat -gc <pid> 5s
Fix now
Switch to LinkedHashMap with removeEldestEntry() for LRU eviction, or use Caffeine cache.
Java Collections Quick Reference
Collection TypeAllows DuplicatesOrdered/SortedNull Keys/ValuesThread-SafeTypical Time Complexity
ArrayListYesInsertion orderYes (values)No — use Collections.synchronizedList()get O(1), add O(1) amortised, remove O(n)
LinkedListYesInsertion orderYesNoget O(n), add/remove at ends O(1)
HashSetNoNo order guaranteedOne null allowedNo — use ConcurrentHashMap.newKeySet()add/contains/remove O(1) average
LinkedHashSetNoInsertion orderOne null allowedNoadd/contains/remove O(1) average
TreeSetNoSorted (natural or Comparator)No null keysNoadd/contains/remove O(log n)
HashMapN/A (keys unique)No orderOne null key, multiple null valuesNo — use ConcurrentHashMapget/put O(1) average
LinkedHashMapN/A (keys unique)Insertion or access orderOne null key, multiple null valuesNoget/put O(1) average
TreeMapN/A (keys unique)Keys sortedNo null keysNoget/put O(log n)
ArrayDequeYesFIFO or LIFONo nulls allowedNoadd/remove ends O(1)
PriorityQueueYesHeap order (not FIFO)No null valuesNo — use PriorityBlockingQueueoffer/poll O(log n), peek O(1)
ConcurrentHashMapN/A (keys unique)No orderNo null keys or valuesYes — lock-free readsget/put O(1) average

Key takeaways

1
Map is NOT a Collection
it has its own hierarchy and doesn't extend Collection or Iterable. Say this confidently in interviews.
2
Always override hashCode() AND equals() together on any class used as a Map key
missing one causes silent, nightmarish bugs where get() returns null for keys that 'should' exist.
3
Fail-fast iterators (ArrayList, HashMap) throw ConcurrentModificationException when you modify the collection mid-iteration
use removeIf() or iterator.remove() to stay safe.
4
LinkedList is almost never the right answer in modern Java
prefer ArrayList for lists and ArrayDeque for queue/stack operations due to cache-friendly memory layout.
5
ConcurrentHashMap uses lock-free reads and per-bucket locking
never use Hashtable in new code.
6
Pre-size collections when you know the expected size to avoid costly resizing or rehashing.

Common mistakes to avoid

5 patterns
×

Overriding equals() without hashCode() in a Map key

Symptom
map.get(key) returns null even though the key 'looks' identical in a debugger. The HashMap puts keys into a bucket based on hashCode, so different hashCodes mean different buckets — equals() is never called.
Fix
Always override both equals() and hashCode() together. Use Objects.hash() for a reliable hash and Objects.equals() in equals().
×

Modifying a Collection inside a for-each loop (single-thread)

Symptom
ConcurrentModificationException thrown at runtime. The loop uses an iterator that checks modCount; any structural modification outside the iterator breaks the contract.
Fix
Use Iterator.remove() for removal during iteration, or Collection.removeIf(predicate) for condition-based removal. Never call add()/remove() directly inside a for-each.
×

Choosing HashMap when iteration order matters

Symptom
Audit logs, reports, or API responses return fields in unpredictable order, causing flaky tests and user complaints. HashMap explicitly does not guarantee order.
Fix
Use LinkedHashMap to preserve insertion order (no extra API changes — it extends HashMap), or TreeMap if keys need to be sorted. Always document the order guarantee in variable names.
×

Using Hashtable or synchronizedMap in new code

Symptom
Poor concurrency performance under load. Hashtable synchronizes every method, serialising all access. ConcurrentHashMap is far more scalable.
Fix
Replace Hashtable with ConcurrentHashMap. For other collections, use CopyOnWriteArrayList or ConcurrentLinkedDeque instead of Collections.synchronizedList().
×

Not pre-sizing collections when the size is known

Symptom
Excessive resizing (rehashing) during HashMap population, causing CPU spikes and temporary memory doubling. ArrayList resizing copies the entire array.
Fix
For ArrayList: new ArrayList<>(expectedSize). For HashMap: initialCapacity = (int)(expectedSize / 0.75f) + 1. For HashSet: new HashSet<>(expectedSize) — but note that HashSet's internal HashMap also needs capacity.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Design a Least Recently Used (LRU) Cache using only the Java Collections...
Q02SENIOR
Given a HashMap where multiple keys collide into the same bucket, walk m...
Q03SENIOR
Explain why ConcurrentHashMap does not use a global lock. How does it ac...
Q04SENIOR
What is the difference between fail-fast and fail-safe iterators? Give e...
Q05JUNIOR
How does HashMap handle null keys?
Q01 of 05SENIOR

Design a Least Recently Used (LRU) Cache using only the Java Collections Framework. Hint: Explore the constructor and protected methods of LinkedHashMap.

ANSWER
LinkedHashMap has a constructor (int initialCapacity, float loadFactor, boolean accessOrder) that, when accessOrder=true, orders entries by last access. Override removeEldestEntry(Map.Entry eldest) to return true when size() exceeds capacity. This gives you an O(1) LRU cache with minimal code. The removeEldestEntry is called at the end of put() and putAll().
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
How does ConcurrentHashMap achieve high concurrency without locking the whole map?
02
Why is the capacity of a HashMap always a power of two?
03
What is the difference between Collection and Collections in Java?
04
When should I use a Set instead of a List in Java?
05
Why does HashMap allow one null key but Hashtable doesn't?
06
What is the worst-case time complexity of get() in a HashMap under bad hashCode()?
🔥

That's Java Interview. Mark it forged?

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

Previous
Java OOP Interview Questions
3 / 6 · Java Interview
Next
Java Multithreading Interview Q&A