Definition
- Interfaces define abstract data types:
Collection(parent of List, Set, Queue) andMap(key-value pairs, separate hierarchy) - Implementations are concrete classes:
ArrayList,LinkedList,HashSet,TreeSet,HashMap,TreeMap,PriorityQueue, and others - Algorithms are static methods in the
Collectionsutility class: sort, binarySearch, shuffle, reverse, unmodifiableList, synchronizedMap - The framework lives in
java.utiland 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.
Collectionis the root, withList,Set, andQueueas sub-interfaces.Mapstands alone – it does not extendCollectionbecause 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
Collectionsutility class –sort,binarySearch,shuffle,reverse,unmodifiableList,synchronizedMap.
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);
}
}
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; providesiterator()methodCollection– addssize(),add(),remove(),contains(),toArray()List– ordered, indexed, duplicates allowed. Addsget(index),set(index),indexOf()Set– no duplicates. No new methods beyond Collection, but the contract changes:add()returns false if the element already existsQueue– FIFO processing. Addsoffer(),poll(),peek()Deque– double-ended queue. AddsofferFirst(),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().
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
}
}
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 |
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());
}
}
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, you can later change ArrayList to LinkedList or CopyOnWriteArrayList without touching any code that uses the accounts variable. If you write 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.
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"));
}
}
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.
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);
}
}
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.
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());
}
}
}
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(), orset()throwsUnsupportedOperationException. - Null-hostile – passing null throws
NullPointerExceptionimmediately. - Duplicate-hostile (for Set.of and Map.of) – duplicate elements or keys throw
IllegalArgumentException. - Serializable – they can be sent across the network or stored.
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());
}
}
}
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 |
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
}
}
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
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);
}
}
Error: ConcurrentModificationException
After removal: [SAV2001, FD4001]
After removeIf: [SAV2001, FD4001]
Mistake 2: Mutable keys in HashMap
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
}
}
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:
- Synchronized wrappers –
Collections.synchronizedList(),Collections.synchronizedMap(). Simple but slow because every operation acquires the same lock. - Concurrent collections –
ConcurrentHashMap,CopyOnWriteArrayList,ConcurrentLinkedQueue. Designed for concurrency with fine-grained locking or lock-free algorithms. - External synchronization – you manage the locking yourself using
synchronizedblocks orReentrantLock.
In a banking system, use ConcurrentHashMap for shared caches and CopyOnWriteArrayList for event listener registries where reads vastly outnumber writes.
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
}
}
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).
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
ConcurrentModificationExceptionif 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
HashMapwith 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 providesfloorKey(),ceilingKey(),headMap(),tailMap(), andsubMap(). 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
┌─────────────────────────────────────────────────────┐ │ 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.
---