HashMap O(n) Bug — Java Collections Interview Questions
95% CPU spike from a HashMap O(n) bug? Fix it by overriding equals() and hashCode().
- 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.
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.
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.
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.
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.
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').
merge() and computeIfAbsent() over put() for atomic updates.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 unchangedKey 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().Frequently Asked Questions
That's Java Interview. Mark it forged?
4 min read · try the examples if you haven't