HashMap O(n) Bug — Java Collections Interview Questions
95% CPU spike from a HashMap O(n) bug? Fix it by overriding equals() and hashCode().
20+ years shipping production code across the stack, with years spent interviewing engineers. Lessons pulled from things that broke in production.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- 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.
This article dissects the HashMap O(n) degradation bug—a classic Java interview trap where a HashMap's get/put operations collapse from O(1) to O(n) due to poor hashCode() distribution or hash collisions. It's not a theoretical edge case; it's a real production failure that surfaces when a custom object's hashCode() returns a constant value, or when an attacker crafts keys to exploit Java 7's linked-list collision handling.
Java 8 mitigated this by converting long collision chains into balanced trees (threshold: 8 entries), but the underlying principle remains: if you don't understand how hashCode() and equals() interact, your HashMap silently becomes a linked list. This article walks through the Collections hierarchy (Collection → Set/List/Queue, Map as a separate root) to show why HashMap exists as a hash-based Map implementation, contrasting it with TreeMap (sorted, O(log n)) and LinkedHashMap (insertion-order).
You'll learn when to pick ArrayList (fast random access, O(1) get) vs LinkedList (fast head/tail inserts, O(1) add/remove) vs HashMap (key-based lookup)—and why misusing them causes real-world performance disasters. The fail-fast vs fail-safe iterator section explains the ConcurrentModificationException trap: fail-fast iterators (ArrayList, HashMap) throw on structural modification during iteration, while fail-safe iterators (CopyOnWriteArrayList, ConcurrentHashMap's keySet) work on a snapshot.
Finally, ConcurrentHashMap's internal design—segmented locks in Java 7, CAS + synchronized on bins in Java 8—is the thread-safe HashMap every senior engineer must know, avoiding full-table locks and providing O(1) concurrency under high contention.
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.
What HashMap O(n) Degradation Really Means
HashMap is a hash-based map offering average O(1) get/put — but worst-case O(n) when hash collisions cascade. The core mechanic: keys are hashed into buckets; multiple keys in one bucket form a linked list (or tree, after threshold). Under poor hashing or deliberate collision attacks, all keys land in one bucket, collapsing performance to linear scan.
In practice, Java 8+ optimizes by converting long bucket chains to balanced trees (threshold 8), raising worst-case to O(log n). But the O(n) risk persists if the hash function is weak or if keys implement hashCode() poorly — e.g., returning a constant. The tree conversion only helps after the chain is already long; initial inserts still suffer O(n) until the threshold is crossed.
Use HashMap for general-purpose key-value lookups where average O(1) suffices and order doesn't matter. Avoid when you need predictable iteration order (use LinkedHashMap) or concurrent access (use ConcurrentHashMap). The O(n) edge case matters in latency-sensitive systems or when accepting untrusted keys — a crafted input can trigger denial-of-service via hash flooding.
HashMap.get() in a long linked-list traversal.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.
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); } }
add() method — it has put() instead.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.
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()); } }
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.
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); } }
remove() or removeIf().Iterator.remove() or removeIf() for safe removal.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.
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); } }
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.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').
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")); } }
merge() and computeIfAbsent() over put() for atomic updates.Why WeakHashMap Exists — And When It Will Bite You
WeakHashMap is the collection that most juniors discover during a memory leak postmortem. The WHY is simple: keys are held by weak references. When no strong reference to a key exists outside the map, the garbage collector reclaims that entry on the next GC cycle. This makes it perfect for caches or metadata attached to objects you don't own the lifecycle of. But here's the trap: the JVM only clears WeakHashMap entries after a full GC. If you're in a low-pause GC like G1, entries can linger for minutes. And if your key has any strong reference chain — static fields, thread locals, event listeners — the weak reference never dies. Use it for ephemeral data. Never for session state or anything that needs deterministic eviction. The 'weak' in the name is about the reference, not the guarantee.
// io.thecodeforge import java.util.*; public class WeakHashMapLeak { private static Map<Object, String> cache = new WeakHashMap<>(); public static void main(String[] args) { Object key = new Object(); // strong ref outside map cache.put(key, "session-data"); key = null; // kill strong ref — entry now eligible System.gc(); // request GC (not guaranteed immediately) // In production with G1GC, this often prints null System.out.println("Post-GC: " + cache.size()); } }
TreeMap vs LinkedHashMap — Ordering Isn't Free
Both preserve order. Both lie about performance. TreeMap keeps keys sorted by their natural ordering or a Comparator. Insert, delete, and lookup are O(log n) because it's a Red-Black tree. LinkedHashMap maintains insertion order (or access order if you flip the constructor flag) using a doubly-linked list running through a hash table. It gives O(1) for basic operations — until hashCode collisions blow the linked list chain length. The payoffs are opposite: TreeMap trades speed for sorted iteration; LinkedHashMap trades memory for predictable iteration order. But here's the killer — LinkedHashMap's access-order mode triggers a structural modification on every get(). If you combine that with fail-fast iteration, you get ConcurrentModificationException even in single-threaded code if you iterate inside a get(). I've debugged three incidents where someone used LinkedHashMap as an LRU cache and hit this. Use access order for caches, not for iteration-sensitive code.
// io.thecodeforge import java.util.*; public class LinkedHashMapTrap { public static void main(String[] args) { LinkedHashMap<String, String> cache = new LinkedHashMap<>(16, 0.75f, true); // access-order cache.put("a", "1"); cache.put("b", "2"); // Bad: iterating while get() triggers reorder for (Map.Entry<String, String> e : cache.entrySet()) { cache.get("a"); // ConcurrentModificationException } } }
Hashmap Degradation in a High-Throughput API
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).- 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).
list.add() or list.remove() called inside the loop. Replace with Iterator.remove() or Collection.removeIf().Collections.synchronizedList() and synchronize on it during iteration.iterator.next() but modCount seems unchangedjcmd <pid> GC.heap_dump /tmp/dump.hprofUse Eclipse MAT OQL: SELECT * FROM java.util.HashMap$Node WHERE bucketIndex = 0grep -r 'ConcurrentModificationException' /var/log/app/*.logUse jstack to capture thread dump and see which threads hold the collection referencejcmd <pid> GC.class_histogram | head -20Monitor heap usage: jstat -gc <pid> 5s| Collection Type | Allows Duplicates | Ordered/Sorted | Null Keys/Values | Thread-Safe | Typical Time Complexity |
|---|---|---|---|---|---|
| ArrayList | Yes | Insertion order | Yes (values) | No — use Collections.synchronizedList() | get O(1), add O(1) amortised, remove O(n) |
| LinkedList | Yes | Insertion order | Yes | No | get O(n), add/remove at ends O(1) |
| HashSet | No | No order guaranteed | One null allowed | No — use ConcurrentHashMap.newKeySet() | add/contains/remove O(1) average |
| LinkedHashSet | No | Insertion order | One null allowed | No | add/contains/remove O(1) average |
| TreeSet | No | Sorted (natural or Comparator) | No null keys | No | add/contains/remove O(log n) |
| HashMap | N/A (keys unique) | No order | One null key, multiple null values | No — use ConcurrentHashMap | get/put O(1) average |
| LinkedHashMap | N/A (keys unique) | Insertion or access order | One null key, multiple null values | No | get/put O(1) average |
| TreeMap | N/A (keys unique) | Keys sorted | No null keys | No | get/put O(log n) |
| ArrayDeque | Yes | FIFO or LIFO | No nulls allowed | No | add/remove ends O(1) |
| PriorityQueue | Yes | Heap order (not FIFO) | No null values | No — use PriorityBlockingQueue | offer/poll O(log n), peek O(1) |
| ConcurrentHashMap | N/A (keys unique) | No order | No null keys or values | Yes — lock-free reads | get/put O(1) average |
| File | Command / Code | Purpose |
|---|---|---|
| io.thecodeforge.collections.HierarchyDemo.java | public class HierarchyDemo { | The Collections Hierarchy |
| io.thecodeforge.collections.LoadFactorDemo.java | public class LoadFactorDemo { | ArrayList vs LinkedList vs HashMap |
| io.thecodeforge.collections.SafeRemoval.java | public class SafeRemoval { | Fail-Fast vs Fail-Safe Iterators |
| io.thecodeforge.collections.ProperKey.java | /** | HashMap Internals |
| io.thecodeforge.collections.ConcurrentHashMapDemo.java | public class ConcurrentHashMapDemo { | ConcurrentHashMap |
| WeakHashMapLeak.java | public class WeakHashMapLeak { | Why WeakHashMap Exists |
| LinkedHashMapTrap.java | public class LinkedHashMapTrap { | TreeMap vs LinkedHashMap |
Key takeaways
equals() together on any class used as a Map keyget() returns null for keys that 'should' exist.iterator.remove() to stay safe.Common mistakes to avoid
5 patternsOverriding equals() without hashCode() in a Map key
equals() is never called.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)
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
Using Hashtable or synchronizedMap in new code
Collections.synchronizedList().Not pre-sizing collections when the size is known
Interview Questions on This Topic
Design a Least Recently Used (LRU) Cache using only the Java Collections Framework. Hint: Explore the constructor and protected methods of LinkedHashMap.
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().Given a HashMap where multiple keys collide into the same bucket, walk me through the transition from a LinkedList to a Red-Black Tree. What is the threshold, and why was it chosen?
Explain why ConcurrentHashMap does not use a global lock. How does it achieve thread-safety across different segments or buckets in Java 8+?
What is the difference between fail-fast and fail-safe iterators? Give examples.
remove() method. Fail-safe iterators (actually 'weakly consistent') work on a snapshot or copy — they don't throw exceptions, but they may not reflect the latest changes. Examples: CopyOnWriteArrayList's iterator, ConcurrentHashMap's iterators. Weakly consistent iterators iterate over the collection as it existed at some point since the iterator was created.How does HashMap handle null keys?
Frequently Asked Questions
ConcurrentHashMap uses a fine-grained locking strategy. In Java 8+, it uses CAS (Compare-And-Swap) operations for empty buckets and synchronizes only on the first node of a bucket (the bin head) when a collision occurs. This allows multiple threads to read and write to different buckets simultaneously without blocking each other.
Using a power of two allows Java to replace the expensive modulo operator (%) with a bitwise AND operator (&) to determine bucket indices. The formula (n - 1) & hash is computationally much cheaper than hash % n, which provides a significant performance boost during frequent put and get operations.
Collection (singular) is an interface — the root of the Collections hierarchy that List, Set, and Queue extend. Collections (plural) is a utility class in java.util that provides static helper methods like Collections.sort(), Collections.unmodifiableList(), and Collections.synchronizedList(). One is a type, the other is a toolbox.
Use a Set whenever uniqueness is a requirement and you don't need positional (index-based) access. The classic use case is deduplication or membership testing — 'has this user already been processed?' A HashSet gives you O(1) contains() versus O(n) for a List. If you need both uniqueness AND ordered iteration, use LinkedHashSet. If you need uniqueness AND sorted order, use TreeSet.
Hashtable predates HashMap and was designed for concurrent use — null keys were disallowed because calling hashCode() on null would throw a NullPointerException inside a synchronised block, causing the lock to be held during the crash. HashMap was designed without thread-safety as a concern, so it handles null keys explicitly as a special case (always bucketed at index 0). For modern concurrent code, use ConcurrentHashMap, which also disallows null keys and values to eliminate ambiguity between 'key not present' and 'key mapped to null'.
Without Java 8 treeification, the worst case is O(n) — all keys hash to the same bucket and are stored in a linked list. With Java 8's tree bin conversion (≥8 entries in a bucket and table size ≥64), worst-case improves to O(log n) because the chain becomes a balanced Red-Black tree.
20+ years shipping production code across the stack, with years spent interviewing engineers. Lessons pulled from things that broke in production.
That's Java Interview. Mark it forged?
5 min read · try the examples if you haven't