Back to Collections Framework Overview
Q1Question 1 of 4

What is the Java Collections Framework?

Key Concepts
  • JCF has three pillars: interfaces (List, Set, Queue, Map), implementations (ArrayList, HashSet, HashMap), and algorithms (Collections.sort, binarySearch, shuffle)
  • Collection is the root interface for List, Set, and Queue; Map is a separate hierarchy and does not extend Collection
  • ArrayList gives O(1) random access, LinkedList gives O(1) insertion at ends, HashSet gives O(1) contains, TreeSet gives O(log n) sorted iteration
  • The Collections utility class provides static methods for sorting, searching, wrapping (unmodifiable, synchronized), and factory creation
  • Java 9 added List.of(), Set.of(), Map.of() for creating immutable collections in one line
  • Always program to the interface (List, Set, Map) rather than the implementation (ArrayList, HashSet, HashMap) so you can swap implementations without changing client code
  • Choose the implementation based on your access pattern: random access vs sequential, ordering vs hashing, uniqueness vs duplicates, thread safety vs speed

Definition

The Java Collections Framework is a unified architecture of interfaces, implementations, and algorithms for storing, retrieving, and manipulating groups of objects in a type-safe, resizable way.
  • Interfaces define abstract data types: Collection (parent of List, Set, Queue) and Map (key-value pairs, separate hierarchy)
  • Implementations are concrete classes: ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap, PriorityQueue, and others
  • Algorithms are static methods in the Collections utility class: sort, binarySearch, shuffle, reverse, unmodifiableList, synchronizedMap
  • The framework lives in java.util and replaces raw arrays with type-safe, resizable, feature-rich data structures
  • Every interface is generic (List, Map) so the compiler catches type errors at compile time rather than runtime

Think of a bank's back office. The bank needs different containers for different purposes. A List is like the ordered queue of customers at the teller counter – it keeps insertion order and the same customer can appear twice if they rejoin the queue. A Set is like the register of unique account numbers – no duplicates allowed, and the main question is always "does this account exist?" A Map is like the account lookup table – you give it an account number (key) and get back the account details (value). The algorithms are the bank's standard operating procedures – sort customers by name, search for an account number, shuffle lottery winners.

Understanding the Concepts

What is the Java Collections Framework?

The Java Collections Framework (JCF) is a set of interfaces, classes, and algorithms introduced in Java 2 (JDK 1.2) that provides a standard way to store and manipulate groups of objects. Before JCF, developers used arrays, Vector, Hashtable, and Stack – each with different APIs and no common interface. JCF unified everything under a single hierarchy.

The framework has three pillars:

  • Interfaces define the contracts. Collection is the root, with List, Set, and Queue as sub-interfaces. Map stands alone – it does not extend Collection because a map holds key-value pairs, not single elements.
  • Implementations are the concrete classes you instantiate. ArrayList, HashSet, HashMap, TreeMap, PriorityQueue, and so on.
  • Algorithms are static methods in the Collections utility class – sort, binarySearch, shuffle, reverse, unmodifiableList, synchronizedMap.
JAVA
package com.bank.demo;

import java.math.BigDecimal;
import java.util.*;

public class CollectionsFrameworkDemo {
    public static void main(String[] args) {
        // List – ordered, allows duplicates
        List<String> accountNumbers = new ArrayList<>();
        accountNumbers.add("SAV2001");
        accountNumbers.add("CUR3001");
        accountNumbers.add("SAV2001"); // duplicate allowed
        System.out.println("List: " + accountNumbers);

        // Set – no duplicates
        Set<String> uniqueAccounts = new HashSet<>(accountNumbers);
        System.out.println("Set: " + uniqueAccounts);

        // Map – key-value pairs
        Map<String, BigDecimal> balances = new HashMap<>();
        balances.put("SAV2001", new BigDecimal("150000.00"));
        balances.put("CUR3001", new BigDecimal("75000.50"));
        System.out.println("Map: " + balances);

        // Queue – FIFO processing
        Queue<String> pendingTransfers = new LinkedList<>();
        pendingTransfers.offer("TXN-001");
        pendingTransfers.offer("TXN-002");
        System.out.println("Queue poll: " + pendingTransfers.poll());

        // Algorithm – sort
        Collections.sort(accountNumbers);
        System.out.println("Sorted: " + accountNumbers);
    }
}
TEXT
List: [SAV2001, CUR3001, SAV2001]
Set: [CUR3001, SAV2001]
Map: {SAV2001=150000.00, CUR3001=75000.50}
Queue poll: TXN-001
Sorted: [CUR3001, SAV2001, SAV2001]

What is the interface hierarchy?

The hierarchy starts with Iterable at the top. Any object that implements Iterable can be used in a for-each loop.

  • Iterable – the top-level interface; provides iterator() method
  • Collection – adds size(), add(), remove(), contains(), toArray()
  • List – ordered, indexed, duplicates allowed. Adds get(index), set(index), indexOf()
  • Set – no duplicates. No new methods beyond Collection, but the contract changes: add() returns false if the element already exists
  • Queue – FIFO processing. Adds offer(), poll(), peek()
  • Deque – double-ended queue. Adds offerFirst(), offerLast(), pollFirst(), pollLast()

Map is a completely separate hierarchy. It does not extend Collection because a map holds key-value pairs, not individual elements. You can get a Set of keys via keySet(), a Collection of values via values(), or a Set> via entrySet().

JAVA
package com.bank.demo;

import java.util.*;

public class HierarchyDemo {
    public static void main(String[] args) {
        // Every Collection is Iterable – works with for-each
        List<String> accounts = List.of("SAV2001", "CUR3001", "FD4001");
        for (String acc : accounts) {
            System.out.println("Account: " + acc);
        }

        // Map is NOT a Collection
        Map<String, String> holders = Map.of("SAV2001", "Rajesh", "CUR3001", "Priya");
        // But you can iterate its entry set, which IS a Collection
        for (Map.Entry<String, String> entry : holders.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }

        // Checking the hierarchy at runtime
        System.out.println(accounts instanceof Collection);  // true
        System.out.println(accounts instanceof Iterable);    // true
        System.out.println(holders instanceof Collection);   // false
        System.out.println(holders instanceof Iterable);     // false
    }
}
TEXT
Account: SAV2001
Account: CUR3001
Account: FD4001
SAV2001 -> Rajesh
CUR3001 -> Priya
true
true
false
false

What are the main implementations and when do you use each one?

Every interface has multiple implementations optimized for different access patterns.

List implementations

| Implementation | Backed by | get(i) | add/remove (middle) | add/remove (end) | Thread-safe | When to use |
|---|---|---|---|---|---|---|
| ArrayList | Dynamic array | O(1) | O(n) | Amortized O(1) | No | Default choice. Random access, iteration |
| LinkedList | Doubly-linked list | O(n) | O(1) if at iterator position | O(1) | No | Frequent insert/remove at both ends |
| Vector | Dynamic array | O(1) | O(n) | Amortized O(1) | Yes (synchronized) | Legacy. Do not use in new code |
| CopyOnWriteArrayList | Array (copied on write) | O(1) | O(n) – copies entire array | O(n) – copies | Yes | Read-heavy concurrent scenarios |

Set implementations

| Implementation | Backed by | add/contains/remove | Ordering | When to use |
|---|---|---|---|---|
| HashSet | HashMap internally | O(1) average | No ordering | Default choice. Fast membership testing |
| LinkedHashSet | LinkedHashMap | O(1) average | Insertion order | Need uniqueness + predictable iteration order |
| TreeSet | Red-black tree | O(log n) | Sorted (natural or Comparator) | Need sorted unique elements |
| EnumSet | Bit vector | O(1) | Enum declaration order | Set of enum constants. Very fast |

Map implementations

| Implementation | Backed by | get/put | Ordering | Null key | When to use |
|---|---|---|---|---|---|
| HashMap | Hash table + linked list/tree | O(1) average | No ordering | One null key | Default choice for key-value lookup |
| LinkedHashMap | HashMap + doubly-linked list | O(1) average | Insertion order (or access order) | One null key | Need predictable iteration order or LRU cache |
| TreeMap | Red-black tree | O(log n) | Sorted by key | No null key | Need sorted keys, range queries |
| Hashtable | Hash table | O(1) average | No ordering | No null key | Legacy. Use ConcurrentHashMap instead |
| ConcurrentHashMap | Segmented hash table | O(1) average | No ordering | No null key/value | Thread-safe concurrent access |

Queue implementations

| Implementation | Backed by | offer/poll | Ordering | When to use |
|---|---|---|---|---|
| LinkedList | Doubly-linked list | O(1) | FIFO | Simple queue |
| ArrayDeque | Resizable circular array | O(1) amortized | FIFO or LIFO | Faster than LinkedList for stack/queue |
| PriorityQueue | Binary heap | O(log n) insert, O(1) peek | Priority (natural or Comparator) | Priority-based processing |

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.*;

public class ImplementationChoiceDemo {
    public static void main(String[] args) {
        // ArrayList – transaction log (ordered, random access)
        List<String> transactionLog = new ArrayList<>();
        transactionLog.add("TXN-001: Deposit 50000");
        transactionLog.add("TXN-002: Withdraw 10000");
        String latest = transactionLog.get(transactionLog.size() - 1);

        // HashSet – unique account number enforcement
        Set<String> activeAccounts = new HashSet<>();
        activeAccounts.add("SAV2001");
        boolean isNew = activeAccounts.add("SAV2001"); // false – already exists
        System.out.println("Added duplicate: " + isNew);

        // HashMap – instant balance lookup
        Map<String, BigDecimal> balances = new HashMap<>();
        balances.put("SAV2001", new BigDecimal("150000.00"));
        BigDecimal bal = balances.get("SAV2001"); // O(1)

        // PriorityQueue – high-value transactions processed first
        PriorityQueue<BigDecimal> priorityQueue = new PriorityQueue<>(
            Comparator.reverseOrder() // highest amount first
        );
        priorityQueue.offer(new BigDecimal("5000"));
        priorityQueue.offer(new BigDecimal("500000"));
        priorityQueue.offer(new BigDecimal("25000"));
        System.out.println("Highest priority: " + priorityQueue.poll());

        // TreeMap – sorted account listing for reports
        TreeMap<String, String> sortedAccounts = new TreeMap<>();
        sortedAccounts.put("FD4001", "Priya");
        sortedAccounts.put("CUR3001", "Rajesh");
        sortedAccounts.put("SAV2001", "Rajesh");
        System.out.println("Accounts in order: " + sortedAccounts.keySet());
    }
}
TEXT
Added duplicate: false
Highest priority: 500000
Accounts in order: [CUR3001, FD4001, SAV2001]

Why should you program to the interface?

Always declare variables using the interface type, not the implementation class. This is one of the most important principles in Java programming.

When you write List accounts = new ArrayList<>(), you can later change ArrayList to LinkedList or CopyOnWriteArrayList without touching any code that uses the accounts variable. If you write ArrayList accounts = new ArrayList<>(), every method that receives this variable is now locked to ArrayList.

This matters in banking systems because requirements change. Today the account list is small and read-heavy (ArrayList is fine). Tomorrow you discover that a thread-safety bug causes data corruption during concurrent updates. If you programmed to the interface, you swap to CopyOnWriteArrayList in one line. If you used the concrete class everywhere, you change every method signature.

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.*;

public class AccountService {
    // Program to the interface – not the implementation
    private final Map<String, BigDecimal> balances;

    public AccountService() {
        // Today: HashMap (fast, not thread-safe)
        this.balances = new HashMap<>();
    }

    // Tomorrow: swap to ConcurrentHashMap in ONE line
    // this.balances = new ConcurrentHashMap<>();
    // Nothing else changes because the type is Map, not HashMap

    public void deposit(String accountId, BigDecimal amount) {
        balances.merge(accountId, amount, BigDecimal::add);
    }

    public BigDecimal getBalance(String accountId) {
        return balances.getOrDefault(accountId, BigDecimal.ZERO);
    }

    public static void main(String[] args) {
        AccountService service = new AccountService();
        service.deposit("SAV2001", new BigDecimal("50000.00"));
        service.deposit("SAV2001", new BigDecimal("25000.00"));
        System.out.println("Balance: " + service.getBalance("SAV2001"));
    }
}
TEXT
Balance: 75000.00

How is Collection different from Collections?

Collection (no 's') is an interface – the root of the List/Set/Queue hierarchy. You implement it when creating a custom collection.

Collections (with 's') is a utility class with static methods that operate on collections. You never instantiate it – you call Collections.sort(), Collections.unmodifiableList(), and so on.

This is one of the most common interview trick questions.

JAVA
package com.bank.demo;

import java.util.*;

public class CollectionVsCollections {
    public static void main(String[] args) {
        // Collection – the INTERFACE
        Collection<String> accounts = new ArrayList<>();
        accounts.add("SAV2001");
        accounts.add("CUR3001");

        // Collections – the UTILITY CLASS
        List<String> accountList = new ArrayList<>(accounts);
        Collections.sort(accountList);
        System.out.println("Sorted: " + accountList);

        List<String> readOnly = Collections.unmodifiableList(accountList);
        // readOnly.add("FD4001"); // throws UnsupportedOperationException

        int count = Collections.frequency(accountList, "SAV2001");
        System.out.println("Frequency of SAV2001: " + count);
    }
}
TEXT
Sorted: [CUR3001, SAV2001]
Frequency of SAV2001: 1

What are the key algorithms in the Collections class?

The Collections class provides algorithms that work on any List or Collection. These are the standard operations you use daily.

  • Sorting: Collections.sort(list) sorts in natural order. Collections.sort(list, comparator) sorts using a custom comparator.
  • Searching: Collections.binarySearch(list, key) finds an element in O(log n) time. The list must be sorted first, or results are undefined.
  • Reordering: reverse(), shuffle(), rotate(), swap() change element positions in place.
  • Aggregation: min(), max(), frequency() compute values across the collection.
  • Wrapping: unmodifiableList() creates a read-only view. synchronizedMap() wraps a map with synchronized access. checkedList() adds runtime type checking.
  • Factory: emptyList(), singletonList(), nCopies() create special-purpose collections.
JAVA
package com.bank.demo;

import java.math.BigDecimal;
import java.util.*;

public class AlgorithmsDemo {
    public static void main(String[] args) {
        List<BigDecimal> transactions = new ArrayList<>(List.of(
            new BigDecimal("50000"),
            new BigDecimal("10000"),
            new BigDecimal("75000"),
            new BigDecimal("25000")
        ));

        // Sort – ascending order
        Collections.sort(transactions);
        System.out.println("Sorted: " + transactions);

        // Binary search – must be sorted first
        int index = Collections.binarySearch(transactions, new BigDecimal("50000"));
        System.out.println("Found 50000 at index: " + index);

        // Min and Max
        System.out.println("Min: " + Collections.min(transactions));
        System.out.println("Max: " + Collections.max(transactions));

        // Reverse
        Collections.reverse(transactions);
        System.out.println("Reversed: " + transactions);

        // Shuffle – random order (useful for lottery, random sampling)
        Collections.shuffle(transactions);
        System.out.println("Shuffled: " + transactions);

        // Unmodifiable wrapper – read-only view
        List<BigDecimal> readOnly = Collections.unmodifiableList(transactions);
        try {
            readOnly.add(new BigDecimal("999"));
        } catch (UnsupportedOperationException e) {
            System.out.println("Cannot modify: " + e.getClass().getSimpleName());
        }
    }
}
TEXT
Sorted: [10000, 25000, 50000, 75000]
Found 50000 at index: 2
Min: 10000
Max: 75000
Reversed: [75000, 50000, 25000, 10000]
Shuffled: [25000, 75000, 10000, 50000]
Cannot modify: UnsupportedOperationException

What about Java 9+ factory methods?

Java 9 introduced List.of(), Set.of(), and Map.of() for creating immutable collections in a single line. These replace the verbose pattern of creating a list, adding elements, and wrapping with Collections.unmodifiableList().

The collections returned by these methods are:

  • Immutable – calling add(), remove(), or set() throws UnsupportedOperationException.
  • Null-hostile – passing null throws NullPointerException immediately.
  • Duplicate-hostile (for Set.of and Map.of) – duplicate elements or keys throw IllegalArgumentException.
  • Serializable – they can be sent across the network or stored.
JAVA
package com.bank.demo;

import java.util.*;

public class FactoryMethodsDemo {
    public static void main(String[] args) {
        // Java 9: List.of – immutable list
        List<String> accounts = List.of("SAV2001", "CUR3001", "FD4001");
        System.out.println("Accounts: " + accounts);

        // Java 9: Set.of – immutable set, no duplicates
        Set<String> branches = Set.of("Mumbai", "Delhi", "Chennai");
        System.out.println("Branches: " + branches);

        // Java 9: Map.of – immutable map (up to 10 entries)
        Map<String, String> accountHolders = Map.of(
            "SAV2001", "Rajesh",
            "CUR3001", "Priya"
        );
        System.out.println("Holders: " + accountHolders);

        // Java 9: Map.ofEntries – for more than 10 entries
        Map<String, String> branches2 = Map.ofEntries(
            Map.entry("BR001", "Mumbai"),
            Map.entry("BR002", "Delhi"),
            Map.entry("BR003", "Chennai")
        );

        // Java 10: List.copyOf – immutable copy of existing collection
        List<String> mutable = new ArrayList<>(List.of("SAV2001", "CUR3001"));
        List<String> immutableCopy = List.copyOf(mutable);
        mutable.add("FD4001"); // original changes
        System.out.println("Mutable: " + mutable);
        System.out.println("Immutable copy: " + immutableCopy); // copy unchanged

        // Null not allowed
        try {
            List<String> bad = List.of("SAV2001", null);
        } catch (NullPointerException e) {
            System.out.println("Null rejected: " + e.getClass().getSimpleName());
        }

        // Duplicate set element not allowed
        try {
            Set<String> bad = Set.of("SAV2001", "SAV2001");
        } catch (IllegalArgumentException e) {
            System.out.println("Duplicate rejected: " + e.getMessage());
        }
    }
}
TEXT
Accounts: [SAV2001, CUR3001, FD4001]
Branches: [Chennai, Delhi, Mumbai]
Holders: {CUR3001=Priya, SAV2001=Rajesh}
Mutable: [SAV2001, CUR3001, FD4001]
Immutable copy: [SAV2001, CUR3001]
Null rejected: NullPointerException
Duplicate rejected: duplicate element: SAV2001

What are the legacy collections and why should you avoid them?

Before JCF existed, Java had Vector, Stack, Hashtable, and Enumeration. These are still in the JDK for backward compatibility, but you should never use them in new code.

| Legacy class | Modern replacement | Why avoid |
|---|---|---|
| Vector | ArrayList (or CopyOnWriteArrayList for thread safety) | Vector synchronizes every method – even single-threaded access pays the synchronization cost |
| Stack | ArrayDeque | Stack extends Vector, inheriting all its synchronization overhead. ArrayDeque is faster and has a cleaner API |
| Hashtable | HashMap (or ConcurrentHashMap for thread safety) | Hashtable synchronizes every method and does not allow null keys or values |
| Enumeration | Iterator | Enumeration has longer method names (hasMoreElements, nextElement) and does not support remove() |
| Dictionary | Map | Dictionary is an abstract class that Hashtable extends. Map is the modern interface |

JAVA
package com.bank.demo;

import java.util.*;

public class LegacyVsModernDemo {
    public static void main(String[] args) {
        // BAD – legacy Vector (synchronized, slow)
        Vector<String> legacyAccounts = new Vector<>();
        legacyAccounts.add("SAV2001");

        // GOOD – modern ArrayList
        List<String> modernAccounts = new ArrayList<>();
        modernAccounts.add("SAV2001");

        // BAD – legacy Stack
        Stack<String> legacyStack = new Stack<>();
        legacyStack.push("TXN-001");

        // GOOD – modern ArrayDeque as stack
        Deque<String> modernStack = new ArrayDeque<>();
        modernStack.push("TXN-001");
        System.out.println("Stack pop: " + modernStack.pop());

        // BAD – legacy Hashtable
        Hashtable<String, String> legacyMap = new Hashtable<>();
        // legacyMap.put("SAV2001", null); // NullPointerException!

        // GOOD – modern HashMap (allows null values)
        Map<String, String> modernMap = new HashMap<>();
        modernMap.put("SAV2001", null); // OK
    }
}
TEXT
Stack pop: TXN-001

What are common mistakes developers make with collections?

The most frequent mistakes involve modifying a collection during iteration, using the wrong equals/hashCode contract, and confusing unmodifiable with immutable.

Mistake 1: ConcurrentModificationException during iteration

JAVA
package com.bank.demo;

import java.util.*;

public class ConcurrentModificationDemo {
    public static void main(String[] args) {
        List<String> accounts = new ArrayList<>(List.of("SAV2001", "CUR3001", "FD4001"));

        // WRONG – modifying during enhanced for-loop
        try {
            for (String acc : accounts) {
                if (acc.startsWith("CUR")) {
                    accounts.remove(acc); // ConcurrentModificationException!
                }
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("Error: " + e.getClass().getSimpleName());
        }

        // CORRECT – use Iterator.remove()
        accounts = new ArrayList<>(List.of("SAV2001", "CUR3001", "FD4001"));
        Iterator<String> it = accounts.iterator();
        while (it.hasNext()) {
            if (it.next().startsWith("CUR")) {
                it.remove(); // safe removal
            }
        }
        System.out.println("After removal: " + accounts);

        // CORRECT – use removeIf (Java 8+)
        accounts = new ArrayList<>(List.of("SAV2001", "CUR3001", "FD4001"));
        accounts.removeIf(acc -> acc.startsWith("CUR"));
        System.out.println("After removeIf: " + accounts);
    }
}
TEXT
Error: ConcurrentModificationException
After removal: [SAV2001, FD4001]
After removeIf: [SAV2001, FD4001]

Mistake 2: Mutable keys in HashMap

JAVA
package com.bank.demo;

import java.util.*;

public class MutableKeyDemo {
    static class Account {
        String id;
        Account(String id) { this.id = id; }
        @Override public int hashCode() { return id.hashCode(); }
        @Override public boolean equals(Object o) {
            return o instanceof Account a && a.id.equals(this.id);
        }
    }

    public static void main(String[] args) {
        Map<Account, String> map = new HashMap<>();
        Account acc = new Account("SAV2001");
        map.put(acc, "Rajesh");
        System.out.println("Before mutation: " + map.get(acc));

        // WRONG – mutating the key after insertion
        acc.id = "CUR3001"; // hashCode changes!
        System.out.println("After mutation: " + map.get(acc)); // null! Lost forever
        System.out.println("Map size: " + map.size()); // still 1, but unreachable
    }
}
TEXT
Before mutation: Rajesh
After mutation: null
Map size: 1

The lesson: always use immutable objects (String, Integer, or immutable records) as map keys.

What about thread safety?

None of the standard JCF implementations (ArrayList, HashSet, HashMap) are thread-safe. If multiple threads read and write the same collection concurrently, you will get corrupted data or ConcurrentModificationException.

You have three options for thread safety:

  1. Synchronized wrappers – Collections.synchronizedList(), Collections.synchronizedMap(). Simple but slow because every operation acquires the same lock.
  2. Concurrent collections – ConcurrentHashMap, CopyOnWriteArrayList, ConcurrentLinkedQueue. Designed for concurrency with fine-grained locking or lock-free algorithms.
  3. External synchronization – you manage the locking yourself using synchronized blocks or ReentrantLock.

In a banking system, use ConcurrentHashMap for shared caches and CopyOnWriteArrayList for event listener registries where reads vastly outnumber writes.

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.*;

public class ThreadSafeCollectionDemo {
    public static void main(String[] args) {
        // Option 1: Synchronized wrapper
        Map<String, BigDecimal> syncMap =
            Collections.synchronizedMap(new HashMap<>());
        syncMap.put("SAV2001", new BigDecimal("50000"));

        // IMPORTANT: iteration still needs external sync
        synchronized (syncMap) {
            for (var entry : syncMap.entrySet()) {
                System.out.println(entry.getKey() + ": " + entry.getValue());
            }
        }

        // Option 2: Concurrent collection (preferred)
        ConcurrentHashMap<String, BigDecimal> concurrentMap = new ConcurrentHashMap<>();
        concurrentMap.put("SAV2001", new BigDecimal("50000"));
        // No external synchronization needed for iteration
        concurrentMap.forEach((k, v) -> System.out.println(k + ": " + v));

        // Atomic operations on ConcurrentHashMap
        concurrentMap.merge("SAV2001", new BigDecimal("10000"), BigDecimal::add);
        System.out.println("After merge: " + concurrentMap.get("SAV2001"));

        // Option 3: CopyOnWriteArrayList for read-heavy scenarios
        List<String> listeners = new CopyOnWriteArrayList<>();
        listeners.add("AuditListener");
        listeners.add("NotificationListener");
        // Safe to iterate while another thread adds/removes
    }
}
TEXT
SAV2001: 50000
SAV2001: 50000
After merge: 60000

What about performance and Big-O complexity?

Choosing the right collection is a performance decision. In a banking system that handles millions of transactions per day, the difference between O(1) and O(n) matters.

| Operation | ArrayList | LinkedList | HashSet | TreeSet | HashMap | TreeMap |
|---|---|---|---|---|---|---|
| get(index) | O(1) | O(n) | N/A | N/A | N/A | N/A |
| add(end) | O(1)* | O(1) | O(1)* | O(log n) | N/A | N/A |
| add(middle) | O(n) | O(1)** | N/A | N/A | N/A | N/A |
| contains | O(n) | O(n) | O(1)* | O(log n) | N/A | N/A |
| put/get | N/A | N/A | N/A | N/A | O(1)* | O(log n) |
| remove | O(n) | O(1)** | O(1)* | O(log n) | O(1)* | O(log n) |
| Iteration | O(n) | O(n) | O(n) | O(n) | O(n) | O(n) |

\* Amortized – occasional O(n) when the internal array or hash table resizes.
\*\* O(1) only if you already have an iterator at the position. Finding the position is O(n).

JAVA
package com.bank.demo;

import java.util.*;

public class PerformanceDemo {
    public static void main(String[] args) {
        int size = 100_000;

        // ArrayList – fast random access
        List<String> arrayList = new ArrayList<>();
        for (int i = 0; i < size; i++) arrayList.add("ACC" + i);
        long start = System.nanoTime();
        String item = arrayList.get(50_000); // O(1)
        long elapsed = System.nanoTime() - start;
        System.out.println("ArrayList.get(50000): " + elapsed + " ns");

        // HashSet – fast contains
        Set<String> hashSet = new HashSet<>(arrayList);
        start = System.nanoTime();
        boolean found = hashSet.contains("ACC50000"); // O(1)
        elapsed = System.nanoTime() - start;
        System.out.println("HashSet.contains: " + elapsed + " ns");

        // ArrayList.contains – slow (O(n) scan)
        start = System.nanoTime();
        found = arrayList.contains("ACC50000"); // O(n)!
        elapsed = System.nanoTime() - start;
        System.out.println("ArrayList.contains: " + elapsed + " ns");
    }
}

What should a senior engineer know?

A senior developer should understand the design decisions behind JCF, not just how to use it.

  • Initial capacity and load factor. HashMap's default initial capacity is 16 and load factor is 0.75. When 12 slots are filled, the table doubles to 32. If you know you will store 10,000 accounts, initialize with new HashMap<>(14000) to avoid repeated resizing. The formula: expectedSize / 0.75 + 1.
  • Treeification in HashMap. Since Java 8, when a single bucket accumulates 8 or more entries (due to hash collisions), HashMap converts that bucket from a linked list to a red-black tree, improving worst-case lookup from O(n) to O(log n). The threshold is TREEIFY_THRESHOLD = 8.
  • Fail-fast vs fail-safe iterators. JCF iterators are fail-fast: they throw ConcurrentModificationException if the collection is structurally modified during iteration (except through the iterator itself). Concurrent collection iterators are weakly consistent (fail-safe): they do not throw exceptions but may not reflect all modifications.
  • Spliterator for parallel processing. Every collection provides a spliterator() method that supports parallel stream decomposition. ArrayList's spliterator can split the underlying array in half, making parallel streams efficient. LinkedList's spliterator cannot split well, so parallel streams on LinkedList are slow.
  • Memory overhead. A HashMap with 1000 entries uses approximately 48 bytes per entry (32 for the Entry object + 16 for the bucket pointer). An ArrayList uses about 4 bytes per element (object reference). Consider memory when choosing data structures for large banking datasets.
  • NavigableMap and NavigableSet. TreeMap implements NavigableMap, which provides floorKey(), ceilingKey(), headMap(), tailMap(), and subMap(). These are powerful for range queries: "find all accounts opened between January and March."

What are common interview follow-up questions?

After this foundational question, interviewers typically drill deeper:

  • "How does HashMap work internally?" – bucket array, hash function, linked list, treeification, rehashing.
  • "What happens if two keys have the same hashCode?" – collision, linked list/tree in the same bucket, equals() distinguishes them.
  • "What is the difference between HashMap and Hashtable?" – synchronization, null handling, fail-fast vs enumerator.
  • "When would you use LinkedHashMap over HashMap?" – when insertion order matters, or for building an LRU cache.
  • "What is the difference between Comparable and Comparator?" – natural ordering vs custom ordering.
  • "How do you make a collection thread-safe?" – synchronized wrappers vs concurrent collections.
  • "What is the difference between fail-fast and fail-safe iterators?" – ConcurrentModificationException vs weakly consistent.
  • "When would you use a TreeMap?" – sorted keys, range queries, NavigableMap operations.

Visual Overview

DIAGRAM
  ┌─────────────────────────────────────────────────────┐
  │                    Iterable<T>                       │
  │                  (for-each loop)                     │
  │                        │                             │
  │                  Collection<T>                       │
  │          (size, add, remove, contains)               │
  │             ┌──────────┼──────────┐                  │
  │             │          │          │                  │
  │          List<T>    Set<T>    Queue<T>               │
  │          ordered    unique     FIFO                  │
  │          indexed               │                    │
  │             │          │     Deque<T>                │
  │             │          │     double-ended             │
  │             │          │                             │
  │  ┌──────────┤    ┌─────┤    ┌─────────┐             │
  │  │ArrayList │    │HashSet   │ArrayDeque│             │
  │  │LinkedList│    │TreeSet   │LinkedList│             │
  │  │Vector    │    │EnumSet   │PriorityQ │             │
  │  └──────────┘    └─────┘    └─────────┘             │
  └─────────────────────────────────────────────────────┘

  ┌─────────────────────────────────────────────────────┐
  │                    Map<K,V>                          │
  │          (SEPARATE hierarchy, not Collection!)       │
  │          (put, get, containsKey, keySet)             │
  │                        │                             │
  │  ┌─────────────────────┼──────────────────┐         │
  │  │HashMap        │LinkedHashMap     │TreeMap│         │
  │  │Hashtable      │ConcurrentHashMap │WeakMap│         │
  │  └───────────────┴─────────────────┴───────┘         │
  └─────────────────────────────────────────────────────┘

  ┌─────────────────────────────────────────────────────┐
  │               Collections (utility)                  │
  │                                                      │
  │  Sorting:    sort(), reverseOrder()                  │
  │  Searching:  binarySearch(), min(), max()            │
  │  Reordering: reverse(), shuffle(), rotate()          │
  │  Wrapping:   unmodifiableList(), synchronizedMap()   │
  │  Factory:    emptyList(), singletonList(), nCopies() │
  └─────────────────────────────────────────────────────┘

Key Takeaway

The Java Collections Framework replaced the chaos of raw arrays, Vector, and Hashtable with a clean, unified architecture. At its core are three pillars. Interfaces define the contract: List for ordered data with duplicates, Set for uniqueness, Queue for FIFO processing, and Map for key-value lookup. Implementations provide the actual storage: ArrayList for fast random access, HashSet for O(1) membership testing, HashMap for instant key-value lookup, TreeMap for sorted keys. Algorithms in the Collections utility class handle the operations that work across all collections: sort, search, shuffle, and wrapping for immutability or thread safety.

In the National Bank of India system, Rajesh uses ArrayList for the transaction log because order matters and the same amount can appear multiple times. He uses HashSet for the active account registry because each account number must be unique and the most common operation is checking whether an account exists. He uses HashMap for the instant balance lookup table where the key is the account number and the value is the BigDecimal balance. For the priority transaction queue, he uses PriorityQueue so high-value transfers get processed first.

The most important principle is programming to the interface. Declare variables as List, Set, or Map – never as ArrayList, HashSet, or HashMap. This lets you swap implementations when requirements change. Today the balance map is a HashMap; tomorrow, when you discover a concurrency bug in production, you change it to ConcurrentHashMap in one line.

Remember one sentence: JCF is interfaces plus implementations plus algorithms, and you always program to the interface because the implementation is a decision you want to be able to change.

---

Share
Use ← → arrow keys to navigate