Senior 11 min · March 05, 2026

Java HashMap Infinite Loop — Why Production Threads Hang

HashMap concurrent resize creates circular linked lists, hanging threads with no exception.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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.
Plain-English First

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.

io/thecodeforge/collections/CollectionHierarchyDemo.javaJAVA
1
2
3
4
5
6
7
8
9
package io.thecodeforge.collections;

import java.util.*;

public class CollectionHierarchyDemo {

    public static void main(String[] args) {

        // --- LIST: ordered
Output
Cart (List): [Apples, Bananas, Apples]
Item at index 1: Bananas
Tags (Set): [java, backend]
Word counts (Map): {hello=5
Pro Tip:
Always declare collection variables using the interface type on the left (List, Set, Map) and the concrete class on the right (new ArrayList<>(), new HashSet<>()). This is called 'programming to interfaces' and it lets you swap implementations in one place without a ripple of changes.
Production Insight
The biggest production trap of the hierarchy is getting confused between Collection and Map.
Map does NOT extend Collection, so you can't pass a Map to a method expecting Collection.
Senior engineers know to check the interface hierarchy first when swapping implementations.
Key Takeaway
Program to interfaces, not implementations.
List<String> list = new ArrayList<>() gives you flexibility.
HashMap<String,String> map = new HashMap<>() locks you into one implementation.

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.

Production Insight
Understanding the hierarchy prevents class cast errors when passing collections across APIs. For example, a method expecting Map won't accept a List because they share no common type — even though both are part of the framework.
Key Takeaway
Collection and Map are entirely separate hierarchies. Know where each interface sits so you can reason about method signatures and polymorphism.

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.

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

import java.util.*;

public class RightImplementationDemo {

    public static void main(String[] args) {

        // SCENARIO 1: Leaderboard that must stay sorted by score (player name -> score)
        // TreeMap automatically keeps keys in natural (alphabetical) order
        Map<String, Integer> leaderboard = new TreeMap<>();
        leaderboard.put("zara",   8500);
        leaderboard.put("alice",  9200);
        leaderboard.put("marcus", 7800);
        System.out.println("Sorted leaderboard: " + leaderboard);
        // Output will always be alphabetically sorted by name — TreeMap guarantees this

        // SCENARIO 2: Recently-visited pages cache (insertion order matters for eviction)
        // LinkedHashMap preserves the order in which entries were added
        Map<String, String> recentPages = new LinkedHashMap<>();
        recentPages.put("/home",    "Home Page");
        recentPages.put("/profile", "User Profile");
        recentPages.put("/settings","Settings");
        System.out.println("Recent pages (insertion order): " + recentPages.keySet());

        // SCENARIO 3: Fast duplicate-free tag lookup — order irrelevant
        // HashSet gives the best raw performance when you only need contains() speed
        Set<String> permissionSet = new HashSet<>(Arrays.asList(
            "READ", "WRITE", "DELETE", "READ" // duplicate READ is silently dropped
        ));
        // O(1) lookup — this is much faster than scanning a List
        System.out.println("Has DELETE permission: " + permissionSet.contains("DELETE"));
        System.out.println("Has ADMIN permission:  " + permissionSet.contains("ADMIN"));

        // SCENARIO 4: Measuring ArrayList vs LinkedList for front-insertions
        List<Integer> arrayBased  = new ArrayList<>();
        List<Integer> linkedBased = new LinkedList<>();

        long startTime = System.nanoTime();
        for (int i = 0; i < 100_000; i++) {
            arrayBased.add(0, i); // insert at index 0 forces shifting every element
        }
        long arrayTime = System.nanoTime() - startTime;

        startTime = System.nanoTime();
        for (int i = 0; i < 100_000; i++) {
            ((LinkedList<Integer>) linkedBased).addFirst(i); // O(1) pointer update
        }
        long linkedTime = System.nanoTime() - startTime;

        System.out.printf("Front-insert 100k: ArrayList=%dms  LinkedList=%dms%n",
            arrayTime  / 1_000_000,
            linkedTime / 1_000_000);
    }
}
Output
Sorted leaderboard: {alice=9200, marcus=7800, zara=8500}
Recent pages (insertion order): [/home, /profile, /settings]
Has DELETE permission: true
Has ADMIN permission: false
Front-insert 100k: ArrayList=312ms LinkedList=8ms
Watch Out:
ArrayList's add(index, element) at position 0 is O(n) — it has to shift every existing element right. On 100k insertions this becomes catastrophically slow. If your access pattern is 'always insert at the front or middle', LinkedList or an ArrayDeque will save you orders of magnitude in latency.
Production Insight
Choosing the wrong implementation is a silent performance killer.
ArrayList front-insertions on a production list of 100k elements added 300ms per operation.
LinkedList's addFirst was 40x faster with no code change other than the constructor.
Always benchmark before optimising — but know the O() of your common operations.
Key Takeaway
ArrayList for random access, LinkedList for frequent front insertions.
HashMap for general key-value lookup, TreeMap for sorted keys.
Your default: ArrayList, HashMap, HashSet. Only deviate when you have a measured reason.

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.

Implementationadd(append)add(index)remove(index)get(index)contains()next (iteration)
ArrayListO(1) amort.O(n)O(n)O(1)O(n)O(1)
LinkedListO(1)O(1) (head)O(1) (head)O(n)O(n)O(1) (via next)
ArrayDequeO(1)N/AN/AO(1)O(n)O(1)
HashSetO(1)N/AO(1)N/AO(1) avgO(n)
TreeSetO(log n)N/AO(log n)N/AO(log n)O(n) (sorted)
HashMapO(1) avgN/AO(1)O(1)O(1) (key)O(n)
TreeMapO(log n)N/AO(log n)O(log n)O(log n)O(n) (sorted)
Key observations
  • 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.

Interview Tip:
When asked about performance trade-offs, use this table as your mental framework. Be ready to explain why ArrayDeque is preferable to LinkedList in many cases (less memory per element, contiguous storage).
Production Insight
The performance gap between O(1) and O(n) can mean the difference between serving 10,000 requests per second and 10. Always profile under realistic load — but use this table as your design-time compass.
Key Takeaway
Big-O complexity guides implementation choice: O(1) for reads (ArrayList, HashMap), O(log n) for sorted operations (Tree*), O(n) for linear scans. Choose based on your dominant operation.

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.

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

import java.util.*;

public class CollectionsUtilityDemo {

    public static void main(String[] args) {
        List<String> names = new ArrayList<>(List.of("Zara", "Alice", "Marcus", "Bob"));

        // sort alphabetically
        Collections.sort(names);
        System.out.println("Sorted: " + names);

        // binary search (list must be sorted)
        int pos = Collections.binarySearch(names, "Marcus");
        System.out.println("Position of Marcus: " + pos);

        // shuffle randomly
        Collections.shuffle(names);
        System.out.println("Shuffled: " + names);

        // create an immutable view
        List<String> immutableView = Collections.unmodifiableList(names);
        try {
            immutableView.add("Someone");
        } catch (UnsupportedOperationException e) {
            System.out.println("Cannot modify immutable view: " + e.getMessage());
        }

        // reverse the list
        Collections.reverse(names);
        System.out.println("Reversed: " + names);

        // frequency count
        int count = Collections.frequency(names, "Alice");
        System.out.println("Frequency of Alice: " + count);
    }
}
Output
Sorted: [Alice, Bob, Marcus, Zara]
Position of Marcus: 2
Shuffled: [Zara, Bob, Alice, Marcus] (random order)
Cannot modify immutable view: null
Reversed: [Marcus, Alice, Bob, Zara]
Frequency of Alice: 1
Pro Tip:
Use 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.
Production Insight
In production, 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.
Key Takeaway
The Collections utility class saves time and reduces bugs. Master sort, binarySearch, shuffle, and unmodifiable wrappers — they are the Swiss Army knife of collection manipulation.

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:

  1. 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.
  2. 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.
  3. 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.
  4. BlockingQueue implementations (ArrayBlockingQueue, LinkedBlockingQueue): For producer-consumer patterns, these handle the synchronisation and blocking semantics automatically.
When to use which
  • 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).
io/thecodeforge/collections/ThreadSafeCollectionDemo.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
package io.thecodeforge.collections;

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

public class ThreadSafeCollectionDemo {

    public static void main(String[] args) throws InterruptedException {
        // --- ConcurrentHashMap ---
        ConcurrentMap<String, Integer> scores = new ConcurrentHashMap<>();
        scores.put("Alice", 90);
        scores.put("Bob", 85);

        // Atomic operation: put if absent
        scores.putIfAbsent("Alice", 100); // does nothing
        System.out.println("ConcurrentHashMap: " + scores);

        // --- CopyOnWriteArrayList ---
        List<String> listeners = new CopyOnWriteArrayList<>();
        listeners.add("Listener1");
        listeners.add("Listener2");

        // Safe iteration even during modification
        for (String listener : listeners) {
            if (listener.equals("Listener2")) {
                listeners.remove(listener); // no ConcurrentModificationException
            }
        }
        System.out.println("CopyOnWriteArrayList after removal: " + listeners);

        // --- Synchronized wrapper ---
        List<String> syncList = Collections.synchronizedList(new ArrayList<>());
        syncList.add("X");
        syncList.add("Y");
        // Manual synchronisation required for compound operations
        synchronized (syncList) {
            Iterator<String> it = syncList.iterator();
            while (it.hasNext()) {
                String s = it.next();
                if (s.equals("Y")) it.remove();
            }
        }
        System.out.println("SynchronizedList after compound operation: " + syncList);

        // --- BlockingQueue example ---
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
        queue.put("Job1");
        String job = queue.take();
        System.out.println("BlockingQueue: took " + job);
    }
}
Output
ConcurrentHashMap: {Alice=90, Bob=85}
CopyOnWriteArrayList after removal: [Listener1]
SynchronizedList after compound operation: [X]
BlockingQueue: took Job1
Common Pitfall:
Synchronized wrappers make individual methods thread-safe, but compound operations like if (!map.containsKey(key)) map.put(key, value) are still racy. Use ConcurrentHashMap's putIfAbsent or computeIfAbsent for atomic compound operations.
Production Insight
The most common production concurrency bug with collections is using a HashMap in a multi-threaded context, often because 'it was never updated concurrently in development'. Use ConcurrentHashMap as your default map in any code that may be called from multiple threads — the overhead is minimal and the safety is invaluable.
Key Takeaway
For thread-safe collections: ConcurrentHashMap for maps, CopyOnWriteArrayList for read-heavy lists, synchronized wrappers only when you understand compound operation risks. Always prefer atomic methods over external locking.

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.

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

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

public class ModernCollectionOps {

    record Product(String name, String category, double price) {}

    public static void main(String[] args) {

        List<Product> inventory = new ArrayList<>(List.of(
            new Product("Laptop",     "Electronics", 1200.00),
            new Product("Headphones", "Electronics",   89.99),
            new Product("Desk Chair", "Furniture",    349.00),
            new Product("USB Hub",    "Electronics",   34.50),
            new Product("Bookshelf",  "Furniture",    179.00)
        ));

        // --- Filter + transform + collect into a new List ---
        // Find names of Electronics under $100, sorted alphabetically
        List<String> affordableElectronics = inventory.stream()
            .filter(p -> p.category().equals("Electronics")) // keep only Electronics
            .filter(p -> p.price() < 100.0)                  // keep only cheap ones
            .map(Product::name)                               // extract just the name
            .sorted()                                         // alphabetical order
            .collect(Collectors.toList());                    // materialise the result

        System.out.println("Affordable electronics: " + affordableElectronics);

        // --- Group products by category using Collectors.groupingBy ---
        Map<String, List<Product>> byCategory = inventory.stream()
            .collect(Collectors.groupingBy(Product::category));

        byCategory.forEach((category, products) -> {\n            double total = products.stream()\n                .mapToDouble(Product::price)\n                .sum();\n            System.out.printf(\"%-15s %d items  total=$%.2f%n\",\n                category, products.size(), total);\n        });\n\n        // --- removeIf: safe in-place removal without ConcurrentModificationException ---\n        // Remove all Furniture items directly from the list\n        inventory.removeIf(p -> p.category().equals(\"Furniture\"));\n        System.out.println(\"\\nAfter removing Furniture:\");\n        inventory.forEach(p -> System.out.println(\"  \" + p.name() + \" - $\" + p.price()));\n\n        // --- Optional to safely handle a missing element ---\n        Optional<Product> mostExpensive = inventory.stream()\n            .max(Comparator.comparingDouble(Product::price));\n\n        // orElseThrow makes the absence explicit — no silent null returns\n        mostExpensive.ifPresent(p ->\n            System.out.println(\"\\nMost expensive remaining: \" + p.name()));\n    }\n}",
        "output": "Affordable electronics: [Headphones, USB Hub]\nElectronics      3 items  total=$1324.49\nFurniture        2 items  total=$528.00\n\nAfter removing Furniture:\n  Laptop - $1200.0\n  Headphones - $89.99\n  USB Hub - $34.5\n\nMost expensive remaining: Laptop"
      }

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.

io/thecodeforge/collections/FailFastFailSafeDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
package io.thecodeforge.collections;

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

public class FailFastFailSafeDemo {

    public static void main(String[] args) {
        // --- Fail-fast with ArrayList ---
        List<String> list = new ArrayList<>(List.of("A", "B", "C"));
        try {\n            for (String s : list) {\n                if (s.equals(\"B\")) {\n                    list.remove(s); // Throws ConcurrentModificationException\n                }\n            }\n        } catch (ConcurrentModificationException e) {\n            System.out.println(\"Fail-fast: \" + e);\n        }\n\n        // --- Fail-safe with CopyOnWriteArrayList ---\n        List<String> cowList = new CopyOnWriteArrayList<>(List.of(\"A\", \"B\", \"C\"));\n        for (String s : cowList) {\n            if (s.equals(\"B\")) {\n                cowList.remove(s); // Works fine — iterator is on a snapshot\n            }\n        }\n        System.out.println(\"CopyOnWriteArrayList after removal: \" + cowList);\n\n        // --- ConcurrentHashMap iterator is weakly consistent ---\n        ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();\n        map.put(1, \"one\");\n        map.put(2, \"two\");\n        for (Integer key : map.keySet()) {\n            if (key == 2) {\n                map.put(3, \"three\"); // iterating thread won't see this\n            }\n        }\n        System.out.println(\"ConcurrentHashMap keys after iteration: \" + map.keySet());\n        // Output will show key 3 present, but inner loop did not process it.\n    }\n}",
        "output": "Fail-fast: java.util.ConcurrentModificationException\nCopyOnWriteArrayList after removal: [A, C]\nConcurrentHashMap keys after iteration: [1, 2, 3]"
      }

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.

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

import java.util.*;

public class EqualsHashCodeDemo {

    static class Person {
        String name;
        int age;

        Person(String name, int age) {\n            this.name = name;\n            this.age = age;\n        }

        // INCORRECT: Only equals overridden, not hashCode
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Person)) return false;
            Person p = (Person) o;
            return age == p.age && Objects.equals(name, p.name);
        }

        // Commented out — this causes the bug
        // @Override
        // public int hashCode() {
        //     return Objects.hash(name, age);
        // }
    }

    public static void main(String[] args) {
        Map<Person, String> map = new HashMap<>();
        Person p1 = new Person("Alice", 30);
        Person p2 = new Person("Alice", 30);

        map.put(p1, "Engineer");
        System.out.println(map.get(p1)); // "Engineer"
        System.out.println(map.get(p2)); // null! because no hashCode override -> default Object hashCode differs
        // Also, map.size() might be 2 because p1 and p2 hash to different buckets.
    }
}
Output
Engineer
null
Mental Model: Library Index
  • 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).
Production Insight
Omitting hashCode() on a key class is the most common collection bug in production.
Symptoms: map.size() grows larger than expected, get() returns null for equal keys.
Use Objects.hash() to generate good hash codes.
Always use immutable or effectively immutable fields in hashCode().
Key Takeaway
Override equals() AND hashCode() together.
Use the same fields in both methods.
Never mutate the fields used in hashCode() while the object is in a HashSet or HashMap.

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.

Production Insight
These problems mirror real-world scenarios: word frequency is a log-analysis task, LRU cache is a core component in database connection pooling, and the event bus appears in GUI frameworks and microservice event handling. Mastering them reduces the friction of translating business requirements to collection usage.
Key Takeaway
Practice problems bridge theory and application. Work through them to internalise when to use List vs Set vs Map, and how to handle concurrency and ordering.
● Production incidentPOST-MORTEMseverity: high

HashMap Infinite Loop in Production — The Silent Crash

Symptom
Application becomes unresponsive under moderate load. Thread dumps show a thread stuck in HashMap.get() or put(). No OutOfMemoryError, no exceptions.
Assumption
HashMap is thread-safe because it's used only for read-heavy workloads with occasional writes.
Root cause
In JDK 7 and earlier, concurrent resize operations on a HashMap could create a circular linked list, causing infinite loops during get() or put(). In JDK 8+, concurrent modifications can cause data loss (silent overwrites) but no infinite loop — still dangerous.
Fix
Replace HashMap with ConcurrentHashMap. If legacy code constrains you to HashMap, wrap it with Collections.synchronizedMap() and synchronise on the map for all compound operations (put-if-absent, iterate-and-modify).
Key lesson
  • 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.
Production debug guideSymptom → action grid for the problems that hit production most often.4 entries
Symptom · 01
ConcurrentModificationException during iteration
Fix
Switch to an Iterator and call iterator.remove() instead of collection.remove(). Better yet, use removeIf() which handles this internally.
Symptom · 02
Map.get() returns null for a key you know exists
Fix
Check the equals() 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.
Symptom · 03
ArrayList.add(0, element) is unexpectedly slow
Fix
Use LinkedList or ArrayDeque for front insertions. ArrayList requires shifting all elements, O(n). If you need both random access and efficient front insertions, consider a circular buffer (ArrayDeque) or a custom data structure.
Symptom · 04
TreeSet or TreeMap produces wrong order
Fix
Ensure the Comparator or compareTo() is consistent with equals(). If comparator returns 0 for non-equal objects, TreeSet will treat them as duplicates and silently drop one.
★ Debugging Collection Performance and Concurrency Issues in ProductionQuick commands and actions for the most painful collection failures.
CPU spike with no obvious cause — suspect HashMap infinite loop
Immediate action
Take a thread dump using jstack <pid> or kill -3 <pid>
Commands
jstack -l <pid> > threaddump.txt
Look for threads stuck in 'java.util.HashMap.put()' or 'get()'
Fix now
Replace the HashMap with ConcurrentHashMap and redeploy.
OutOfMemoryError with large collections+
Immediate action
Capture a heap dump using jmap
Commands
jmap -dump:live,format=b,file=heap.hprof <pid>
Analyze with Eclipse MAT: look for one collection that dominates the heap
Fix now
If it's an ArrayList that grows unbounded, use a fixed-size ArrayBlockingQueue or a bounded Deque.
Collection operations taking seconds instead of milliseconds+
Immediate action
Enable GC logging and check GC pauses
Commands
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
If GC is not the issue, profile the code with async-profiler or VisualVM
Fix now
If you're using LinkedList for random access, switch to ArrayList. If you're using ArrayList for frequent front insertions, switch to LinkedList.
Collection Implementation Comparison
ImplementationOrdered?Sorted?Duplicates?Null Keys/Values?Thread-Safe?Best Use Case
ArrayListYes (insertion)NoYesYes (values)NoRead-heavy lists, random access by index
LinkedListYes (insertion)NoYesYes (values)NoFrequent front/middle insertions, Queue/Deque use
HashSetNoNoNoOne nullNoFast uniqueness checks, de-duplication
LinkedHashSetYes (insertion)NoNoOne nullNoUnique elements where insertion order matters
TreeSetYes (sorted)Yes (natural/Comparator)NoNoNoSorted unique elements, range queries
HashMapNoNoKeys: No, Values: YesOne null key, many null valuesNoGeneral-purpose key-value lookup
LinkedHashMapYes (insertion)NoKeys: NoOne null keyNoLRU caches, order-preserving maps
TreeMapYes (sorted)Yes (natural/Comparator)Keys: NoNo null keysNoSorted key ranges, leaderboards, scheduling
ConcurrentHashMapNoNoKeys: NoNo nulls at allYesHigh-concurrency read/write scenarios

Key takeaways

1
Always program to the interface (List, Set, Map) on the left side of declarations
concrete types on the right only. This preserves flexibility and is the single habit that separates clean from brittle code.
2
HashMap, ArrayList, and HashSet are your defaults
but the moment you need sorted order, reach for TreeMap/TreeSet; for insertion-order preservation, reach for LinkedHashMap/LinkedHashSet. The wrong choice doesn't fail loudly — it just degrades performance silently.
3
Never remove elements from a collection while iterating it with a for-each
use removeIf() instead. This prevents ConcurrentModificationException and produces cleaner, more readable code in a single step.
4
Map is NOT a subtype of Collection
it's a parallel hierarchy. Knowing this and being able to explain why (key-value pairs have fundamentally different semantics than single-element sequences) is a reliable signal of genuine framework understanding in interviews.
5
Override 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 patterns
×

Removing elements inside a for-each loop

Symptom
ConcurrentModificationException at runtime because the iterator detects structural modification mid-traversal.
Fix
Use removeIf(predicate) for condition-based removal, or collect elements to remove into a separate list and call removeAll() after the loop completes.
×

Declaring variables as concrete types instead of interface types

Symptom
Every method signature that accepts ArrayList<String> must change if you later need a LinkedList or a synchronised wrapper.
Fix
Always declare: List<String> list = new ArrayList<>(); not ArrayList<String>.
×

Using HashMap in a multi-threaded environment without synchronisation

Symptom
Concurrent puts can cause infinite loops in older JDKs and lost updates in all JDKs, with zero exception thrown.
Fix
Use ConcurrentHashMap for concurrent access, or wrap with 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

Symptom
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.
Fix
Override both equals() and hashCode() using the same fields. Use Objects.hash() for a quick implementation.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between ArrayList and LinkedList, and when would ...
Q02SENIOR
Why does Map not extend the Collection interface in Java, even though it...
Q03SENIOR
If you call HashMap.put() with a key that already exists, what happens —...
Q04SENIOR
How does ConcurrentHashMap achieve thread-safety without locking the ent...
Q01 of 04JUNIOR

What is the difference between ArrayList and LinkedList, and when would you choose one over the other in a production system?

ANSWER
ArrayList uses a dynamic array under the hood. It offers O(1) random access by index, but insertions/removals at the front or middle are O(n) because elements must be shifted. LinkedList uses a doubly-linked list. It offers O(1) insertions/removals at the head or tail, but O(n) for random access. In production, choose ArrayList for read-heavy scenarios with occasional direct access by index (like pagination). Choose LinkedList when you frequently add/remove from the ends (like a queue) or when random access is rare. For a queue, also consider ArrayDeque which often outperforms LinkedList and offers O(1) head/tail operations with less overhead. _Practical rule:_ If you don't know which to use, start with ArrayList. It's the most cache-friendly and has the lowest per-element memory overhead.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the Java Collections Framework and why was it introduced?
02
What is the difference between Collection and Collections in Java?
03
Is it safe to use null as a key or value in Java Maps?
04
What is the difference between fail-fast and fail-safe iterators?
05
When should I use parallelStream() instead of stream()?
🔥

That's Collections. Mark it forged?

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

Previous
Multi-catch and Finally Block
1 / 21 · Collections
Next
ArrayList in Java