Java HashMap Infinite Loop — Why Production Threads Hang
HashMap concurrent resize creates circular linked lists, hanging threads with no exception.
- The Collections Framework is a unified library of interfaces and implementations for storing and manipulating groups of objects.
- Core interfaces: Collection (List, Set, Queue) and Map (separate hierarchy).
- Each interface has multiple implementations with different performance trade-offs.
- ArrayList gives O(1) random access; LinkedList gives O(1) head/tail insertions.
- HashMap gives average O(1) lookups but no ordering guarantees.
- Production trap: Using HashMap in a multi-threaded context without synchronisation can cause data corruption or infinite loops.
Imagine you're organising a school library. Sometimes you need a numbered shelf where order matters (List). Sometimes you just need a pile where duplicates aren't allowed (Set). Sometimes you need a lookup card that maps a book title to its shelf location (Map). And sometimes you need a queue of students waiting to borrow a book (Queue). Java's Collections Framework is simply a built-in toolkit of these different 'container' styles — each designed for a specific job, so you're never building the container from scratch.
Every non-trivial Java application manages groups of data — a shopping cart full of products, a leaderboard of player scores, a cache mapping user IDs to session objects. Without a standard way to store, retrieve, and manipulate these groups, every developer would roll their own solutions and those solutions wouldn't talk to each other cleanly. That's chaos at scale, and it's exactly the problem Java's Collections Framework was built to prevent.
Before the Collections Framework landed in Java 1.2, developers had to make do with arrays (rigid, fixed-size) and a handful of thread-specific classes like Vector and Hashtable that were slow because they synchronised everything by default whether you needed it or not. The framework replaced all of that with a clean hierarchy of interfaces and pluggable implementations, letting you swap an ArrayList for a LinkedList, or a HashMap for a TreeMap, without changing a single line of calling code. That's the power of programming to interfaces, and Collections is the poster child for that principle in Java.
By the end of this article you'll understand why the framework is structured the way it is, how to choose the right collection for a real scenario, where each core interface fits in the hierarchy, and the runtime tradeoffs that separate senior developers from juniors in whiteboard interviews. You'll also see the mistakes that burn people in production and exactly how to avoid them.
The Hierarchy That Makes Everything Click
The Collections Framework is built on interfaces, not classes. This is the single most important architectural decision to understand. When you accept a List<String> as a method parameter, you're saying 'I don't care whether you hand me an ArrayList or a LinkedList — I just need something that behaves like a list.' That flexibility is what lets libraries, frameworks, and teammates swap implementations without breaking each other's code.
At the top sits the Iterable interface — anything that can be looped over with a for-each. Beneath it is Collection, which adds size(), contains(), add(), and remove(). From Collection, three major branches split off: List (ordered, index-based, allows duplicates), Set (no duplicates, no guaranteed order unless you choose a sorted variant), and Queue (designed for holding elements prior to processing, with head/tail semantics). Map sits separately because it deals with key-value pairs rather than individual elements — it doesn't extend Collection at all, which surprises a lot of people.
Think of the interface hierarchy as a contract ladder. The higher you program (closer to the interface), the more flexibility you preserve. Never declare a variable as ArrayList<String> when List<String> does the job. You'll thank yourself the day requirements change.
Visual Hierarchy: Collection and Map Interface Trees
The interface hierarchy is easier to internalise when you see it as a tree. At the root sits Iterable, the most general contract. Collection extends Iterable and adds the ability to add, remove, and query size. From Collection, three direct subinterfaces branch off: List, Set, and Queue. Map does not extend Collection; it lives as a separate top-level interface alongside Iterable.
Below the interfaces, concrete implementations provide the actual storage and algorithms. Every implementation honours the contract of its interface, but they differ in performance, ordering guarantees, and thread-safety.
This diagram shows the relationships visually. Keep it in mind whenever you reach for a collection: start from the interface, then pick the implementation based on your access pattern and concurrency requirements.
Choosing the Right Implementation — and Why It Matters at Runtime
Knowing the interfaces is the foundation, but the real skill is knowing which implementation to hand-pick for a given scenario. The wrong choice doesn't cause a compile error — it just quietly kills your performance under load, which is far harder to debug.
For Lists: ArrayList is backed by a resizable array. Random access by index is O(1), which makes it ideal for read-heavy use cases like rendering a paginated result set. LinkedList is backed by a doubly-linked chain of nodes. Insertions and deletions at the front or middle are O(1), but index access requires traversal. Use LinkedList when you're frequently adding or removing from the ends — not for general-purpose storage.
For Maps: HashMap gives O(1) average for get/put but makes zero guarantees about iteration order. LinkedHashMap preserves insertion order, great for LRU caches. TreeMap keeps keys sorted (O(log n) for everything) — use it when you need a sorted key range, like a leaderboard. For Sets, the same pattern mirrors across HashSet, LinkedHashSet, and TreeSet.
For concurrent code, none of the above are thread-safe by default. Reach for ConcurrentHashMap or Collections.synchronizedList() wrapping, or better yet — rethink whether shared mutable state is necessary at all.
O() of your common operations.Performance Comparison: Big-O Complexity for Common Operations
Choosing the right collection often boils down to understanding the time complexity of core operations. The table below summarises the average-case and worst-case big-O for the most frequently used implementations. Use it as a quick reference when designing data paths.
| Implementation | add(append) | add(index) | remove(index) | get(index) | contains() | next (iteration) |
|---|---|---|---|---|---|---|
| ArrayList | O(1) amort. | O(n) | O(n) | O(1) | O(n) | O(1) |
| LinkedList | O(1) | O(1) (head) | O(1) (head) | O(n) | O(n) | O(1) (via next) |
| ArrayDeque | O(1) | N/A | N/A | O(1) | O(n) | O(1) |
| HashSet | O(1) | N/A | O(1) | N/A | O(1) avg | O(n) |
| TreeSet | O(log n) | N/A | O(log n) | N/A | O(log n) | O(n) (sorted) |
| HashMap | O(1) avg | N/A | O(1) | O(1) | O(1) (key) | O(n) |
| TreeMap | O(log n) | N/A | O(log n) | O(log n) | O(log n) | O(n) (sorted) |
- ArrayList random access is unbeatable but insertion/removal at arbitrary positions is expensive.
- LinkedList insertion/removal at head and tail is O(1), but index access is O(n). ArrayDeque also offers O(1) head/tail operations and is more cache-friendly.
- TreeSet and TreeMap provide sorted iteration but pay O(log n) for all operations.
- HashSet and HashMap achieve constant-time average performance but require good hash codes.
Always consider your workload's dominant operation before committing to an implementation.
Collections Utility Class: Sorting, Searching, Shuffling and Immutable Wrappers
The Collections class (note the 's') is a utility toolbox packed with static methods that operate on any Collection implementation. It provides reusable algorithms that would otherwise have to be implemented manually. The most commonly used methods include:
sort(): Sorts a List in natural order or using a Comparator. Under the hood it uses a highly tuned merge sort (or TimSort in newer JDKs). Example:Collections.sort(list).- binarySearch(): Performs a binary search on a sorted List. Returns the index if found, or a negative insertion point. Requires O(log n) time. Example:
int idx = Collections.binarySearch(list, key);. shuffle(): Randomly permutes the elements of a List. Useful for testing or game logic. Example:Collections.shuffle(list);.- unmodifiableList() / unmodifiableSet() / unmodifiableMap(): Returns an immutable view of the collection. Any attempt to modify the view throws UnsupportedOperationException. This is the safest way to expose internal collections to external callers without risking accidental modification.
- synchronizedList() / synchronizedSet() / synchronizedMap(): Wraps a collection with synchronized methods for basic thread-safety. (Covered in the next section.)
reverse(),fill(),copy(),swap(),rotate(),frequency(),disjoint(), etc.
Using these utility methods reduces boilerplate and centralises the algorithm implementation. They are tested, optimised, and maintained by the JDK authors — trust them over hand-rolled loops.
Collections.unmodifiableList() to return read-only views from getters. This prevents callers from mutating your internal state. Combine with immutable objects for true defensive programming.Collections.binarySearch() is often forgotten in favour of a linear scan. For lists larger than a few thousand elements, the difference between O(log n) and O(n) can be milliseconds vs microseconds. Always pre-sort and use binary search when you perform repeated lookups on a static list.Thread-Safe Collections: ConcurrentHashMap, CopyOnWriteArrayList, and Synchronized Wrappers
Most collection implementations are not thread-safe. When accessed from multiple threads, they can produce corrupt internal state, infinite loops, or silent data loss. Java provides several options to handle concurrent access:
- Synchronized Wrappers (Collections.synchronizedList, synchronizedMap, synchronizedSet): These wrap a regular collection with methods that synchronise on the wrapper object. Every individual method call is atomic, but compound operations (check-then-put, iterate-and-modify) require external synchronisation. Use these only when you need thread-safety quickly and are careful to synchronise compound operations manually.
- CopyOnWriteArrayList: Instead of locking, it creates a fresh copy of the underlying array on every write (add, set, remove). Reads are never synchronised — they operate on the current snapshot. This makes it safe for iteration without ConcurrentModificationException, at the cost of O(n) memory per write. Ideal for read-heavy, write-rare workloads like listener registries.
- ConcurrentHashMap: The gold standard for high-concurrency maps. It partitions the map into buckets and locks only the bucket being written to. Reads are lock-free via volatile reads. It provides atomic compound operations like putIfAbsent and computeIfAbsent. No null keys or values allowed. Prefer this over synchronizedMap for any concurrent map access.
- BlockingQueue implementations (ArrayBlockingQueue, LinkedBlockingQueue): For producer-consumer patterns, these handle the synchronisation and blocking semantics automatically.
- Single writer / many readers: CopyOnWriteArrayList.
- High concurrency reads and writes on a map: ConcurrentHashMap.
- Need simple thread-safety quickly with known compound operations: synchronized wrapper (with careful external lock).
if (!map.containsKey(key)) map.put(key, value) are still racy. Use ConcurrentHashMap's putIfAbsent or computeIfAbsent for atomic compound operations.Iterating, Filtering and Transforming Collections the Modern Way
Knowing which collection to use is half the battle. The other half is working with the data inside it efficiently. Pre-Java 8 code was littered with verbose for loops and manual null checks. The Stream API, added in Java 8, plugs directly into any Collection via the stream() method and lets you express what you want, not how to get it.
The key mental model is: a Stream is a pipeline of operations on a sequence of elements. It doesn't modify the original collection — it produces a new result. Operations are either intermediate (filter, map, sorted — they return a Stream and are lazy) or terminal (collect, count, forEach — they trigger evaluation and produce a result). Lazy evaluation means that if you filter a million-element list but only need the first five results, Java won't process elements six through a million.
For in-place modification, use the Collection's own removeIf() or replaceAll() methods — they're cleaner and safer than removing elements inside a for-each loop (which throws ConcurrentModificationException). Knowing the difference between stream operations and in-place mutations is a genuine differentiator in senior-level Java interviews.
Fail-Fast vs Fail-Safe Iterators — What Breaks at Runtime and Why
When you iterate over a collection, you're using an Iterator behind the scenes. The behaviour of that iterator under concurrent modification differs between two families: fail-fast and fail-safe.
Fail-fast iterators (the default for all java.util collection classes except the concurrent ones) throw ConcurrentModificationException the moment they detect structural modification of the collection after the iterator was created. This is a design choice — better to fail immediately than produce silently corrupt results. The detection works by tracking a modCount field: every time you add, remove, or clear, modCount increments. The iterator stores the expected modCount at creation and checks it on every next() call. Mismatch triggers the exception.
Fail-safe iterators (used by ConcurrentHashMap, CopyOnWriteArrayList, and the collections in java.util.concurrent) operate on a snapshot or a copy of the underlying data. They don't throw ConcurrentModificationException because they don't share state with the original collection. The trade-off: you may not see the latest updates in your iteration, and there's a cost to taking the snapshot (memory or CPU).
This distinction matters in production. If you're iterating over a ConcurrentHashMap and you expect to see elements added later — you won't. The iterator only sees the state at creation. For most use cases that's fine, but if you need a live view, you need to re-iterate or redesign the interaction.
equals() and hashCode() — The Contract That Makes Collections Work
Two methods control how HashMap, HashSet, and other hash-based collections identify and store your custom objects: equals() and hashCode(). If you override one without the other, your collections will behave erratically — keys won't be found, duplicates will appear, and equals-based lookups will fail.
The contract is strict: if two objects are equal according to equals(), they must have the same hashCode(). The reverse is not required — two objects can have the same hash code but not be equal (this is a hash collision, which is handled via linked lists or trees in the bucket).
But the more subtle rule: hashCode() should not change after an object is placed in a HashMap or HashSet. If you mutate the object's fields that participate in hashCode() calculation, the bucket index changes, and containsKey() or get() will never find the object again. This leads to memory leaks—objects accumulate in the map without ever being retrievable.
Immutable keys are the safest choice. If you must use mutable keys, never mutate them while they're in a collection, or remove and re-insert after mutation.
- hashCode() determines which shelf a book goes on.
- equals() determines if two books are the same title.
- If
equals()says two books are identical but hashCode() puts them on different shelves, you'll never find the duplicate. - If hashCode() changes after the book is placed on a shelf, the book is lost forever (memory leak).
map.size() grows larger than expected, get() returns null for equal keys.Objects.hash() to generate good hash codes.equals() AND hashCode() together.Practice Problems to Solidify Your Understanding
Apply what you've learned with these five hands-on problems. Each focuses on a different aspect of the Collections Framework. Try solving them without looking up the answers first.
1. Word Frequency Counter Write a method that takes a list of strings and returns a map where each key is a word and the value is its frequency. Use a HashMap and handle case-insensitivity. (Tip: Use merge() or computeIfAbsent for atomic updates.)
2. Shopping Cart with Order Preservation Implement a shopping cart that maintains the order items are added, allows duplicates, and supports fast removal by index. Which List implementation would you choose? (Consider ArrayList or LinkedList? What about a LinkedHashMap with a custom wrapper?) Final answer: Use an ArrayList for O(1) index access unless you remove from the front frequently.
3. Deduplicate with Insertion Order Given a list with duplicates, return a new list containing only the first occurrence of each element, preserving the original order. (Hint: Use a LinkedHashSet to track seen elements, then stream the original list.)
4. Implement an LRU Cache An LRU (Least Recently Used) cache evicts the least recently accessed entry when the cache is full. Use LinkedHashMap with access-order=true and override removeEldestEntry(). (Test with capacity 3 and verify eviction order.)
5. Thread-Safe Event Bus Design a simple event bus that allows multiple threads to register listeners (Runnable) and fire events. Use CopyOnWriteArrayList to store listeners so that firing can occur without blocking. (Implement fire() that iterates over listeners and calls run().)
These problems cover the core patterns: choosing the right collection, using utility methods, handling concurrency, and honouring contracts.
HashMap Infinite Loop in Production — The Silent Crash
HashMap.get() or put(). No OutOfMemoryError, no exceptions.get() or put(). In JDK 8+, concurrent modifications can cause data loss (silent overwrites) but no infinite loop — still dangerous.Collections.synchronizedMap() and synchronise on the map for all compound operations (put-if-absent, iterate-and-modify).- Never assume a non-thread-safe collection is safe just because writes are infrequent.
- Use ConcurrentHashMap for any map accessed from multiple threads, regardless of read/write ratio.
- Always profile thread dumps when you see CPU spikes — they expose the exact call stack.
iterator.remove() instead of collection.remove(). Better yet, use removeIf() which handles this internally.Map.get() returns null for a key you know existsequals() and hashCode() implementations of your key class. If you overrode one but not the other, Map can't find the key. Also verify the key object isn't mutated after being inserted.equals(). If comparator returns 0 for non-equal objects, TreeSet will treat them as duplicates and silently drop one.Key takeaways
equals() and hashCode() together when using custom objects as keys. Never mutate the fields used in hashCode() while the object is in a collection.Common mistakes to avoid
4 patternsRemoving elements inside a for-each loop
Declaring variables as concrete types instead of interface types
Using HashMap in a multi-threaded environment without synchronisation
Collections.synchronizedMap() — but be aware that synchronizedMap still requires manual synchronisation around compound operations like check-then-put.Forgetting to override hashCode() when using custom objects as keys
Map.get() returns null even though you have an equal key in the map. Map size may be larger than expected because equal objects are in separate buckets.equals() and hashCode() using the same fields. Use Objects.hash() for a quick implementation.Interview Questions on This Topic
What is the difference between ArrayList and LinkedList, and when would you choose one over the other in a production system?
Frequently Asked Questions
That's Collections. Mark it forged?
11 min read · try the examples if you haven't