Java Collections Framework
Java Collections Framework
Every real Java program manages groups of objects — a list of orders, a set of unique emails, a map of product prices. Before the Collections Framework existed, developers used raw arrays for everything: fixed size, no built-in search, no sorting, no standard way to pass them between methods. The Collections Framework replaced all of that with a unified, interface-driven architecture that every Java developer uses every day.
Understanding it is not optional. Every Java interview touches it. Every production codebase depends on it.
What Is the Java Collections Framework?
The Java Collections Framework (JCF) is a set of interfaces, implementations, and algorithms in the java.util package that provides standard, reusable data structures for Java programs. It was introduced in Java 1.2 and has been the foundation of data management in Java ever since.
The framework gives you three things:
- ›Interfaces — define what a collection can do (
List,Set,Queue,Map) - ›Implementations — concrete classes you actually instantiate (
ArrayList,HashMap,HashSet,TreeMap) - ›Algorithms — utility methods that work on any collection (
Collections.sort(),Collections.min(),Collections.binarySearch())
The diagram below shows the complete hierarchy. Map is a separate branch because it stores key-value pairs rather than individual elements.
java.lang.Iterable
└── java.util.Collection
├── java.util.List (ordered, allows duplicates)
│ ├── ArrayList (resizable array — default choice)
│ ├── LinkedList (doubly-linked nodes)
│ └── Vector (legacy, synchronized)
│
├── java.util.Set (unique elements, no duplicates)
│ ├── HashSet (hash table — fastest, no order)
│ ├── LinkedHashSet (hash table + insertion order)
│ └── TreeSet (Red-Black tree — sorted order)
│
└── java.util.Queue (FIFO — first in, first out)
├── PriorityQueue (heap — priority-based order)
├── ArrayDeque (double-ended queue — modern default)
└── LinkedList (also implements Queue)
java.util.Map (key-value pairs — separate hierarchy)
├── HashMap (hash table — fastest, no order)
├── LinkedHashMap (hash table + insertion order)
├── TreeMap (Red-Black tree — sorted keys)
└── Hashtable (legacy, synchronized)
The Three Core Questions
Before writing a single line of collection code, answer these three questions in order:
Question 1: Do I need key-value lookup? YES → Use a Map (HashMap, TreeMap, LinkedHashMap) NO → Go to Question 2 Question 2: Do I need uniqueness — no duplicates? YES → Use a Set (HashSet, TreeSet, LinkedHashSet) NO → Go to Question 3 Question 3: Do I need an ordered sequence? YES → Use a List (ArrayList, LinkedList) Or a Queue for FIFO processing (ArrayDeque, PriorityQueue)
This decision tree produces the right collection family for almost every real-world scenario. The implementation choice — ArrayList vs LinkedList, HashMap vs TreeMap — comes after the family is decided.
Why Not Just Use Arrays?
A plain Java array works for fixed, known data. For everything else it creates problems that the Collections Framework solves directly.
1// File: ArrayLimitationsDemo.java
2
3import java.util.ArrayList;
4import java.util.Collections;
5import java.util.List;
6
7public class ArrayLimitationsDemo {
8
9 public static void main(String[] args) {
10
11 System.out.println("=== Problem 1: Arrays have a fixed size ===");
12
13 // Array — must declare size at creation
14 String[] fixedOrders = new String[3];
15 fixedOrders[0] = "ORD-001";
16 fixedOrders[1] = "ORD-002";
17 fixedOrders[2] = "ORD-003";
18 // fixedOrders[3] = "ORD-004"; — compile error! ArrayIndexOutOfBoundsException
19
20 // ArrayList — grows automatically
21 List<String> orders = new ArrayList<>();
22 orders.add("ORD-001");
23 orders.add("ORD-002");
24 orders.add("ORD-003");
25 orders.add("ORD-004"); // no problem — list handles the growth
26 orders.add("ORD-005");
27 System.out.println("Array size (fixed): 3");
28 System.out.println("List size (dynamic): " + orders.size());
29
30 System.out.println();
31 System.out.println("=== Problem 2: Arrays have no built-in algorithms ===");
32
33 // Searching an array — write a manual loop
34 String target = "ORD-003";
35 boolean found = false;
36 for (String order : fixedOrders) {
37 if (target.equals(order)) { found = true; break; }
38 }
39 System.out.println("Manual array search: found=" + found);
40
41 // List — one method call
42 System.out.println("List contains(): found=" + orders.contains("ORD-003"));
43
44 // Sorting an array — Arrays.sort() works but produces no new API
45 // Sorting a List — Collections.sort() + reverse, shuffle, min, max...
46 Collections.sort(orders);
47 System.out.println("Sorted list: " + orders);
48 System.out.println("Min order : " + Collections.min(orders));
49 }
50}Output:
=== Problem 1: Arrays have a fixed size ===
Array size (fixed): 3
List size (dynamic): 5
=== Problem 2: Arrays have no built-in algorithms ===
Manual array search: found=true
List contains(): found=true
Sorted list: [ORD-001, ORD-002, ORD-003, ORD-004, ORD-005]
Min order : ORD-001
When to Use the Collections Framework
Use collections whenever the data you manage has any of these characteristics:
- ›The size is not known at compile time
- ›Elements need to be added or removed dynamically
- ›Fast searching, sorting, or membership testing is needed
- ›The data must be passed across method boundaries with a clean API
- ›Multiple threads need to access the same data structure
Use a plain array only for: fixed-size primitive data where boxing overhead matters (an int[] of 1,000,000 numbers is faster and smaller than List<Integer>), low-level interoperability with APIs that require arrays, or multi-dimensional grid data (int[][]).
The Core Interfaces — What Each One Promises
List — Ordered Sequence
List<E> maintains insertion order and allows duplicate elements. Every List implementation provides index-based access through get(int index).
1// File: ListBasicsDemo.java
2
3import java.util.ArrayList;
4import java.util.List;
5
6public class ListBasicsDemo {
7
8 public static void main(String[] args) {
9
10 // ArrayList is the default List implementation
11 List<String> techStack = new ArrayList<>();
12 techStack.add("Java");
13 techStack.add("Spring Boot");
14 techStack.add("MySQL");
15 techStack.add("Java"); // duplicates allowed
16 techStack.add(1, "Hibernate"); // insert at index 1 — shifts others right
17
18 System.out.println("Tech stack : " + techStack);
19 System.out.println("Size : " + techStack.size());
20 System.out.println("Element at [0] : " + techStack.get(0)); // index access
21 System.out.println("Index of Java : " + techStack.indexOf("Java")); // first occurrence
22 System.out.println("Contains MySQL : " + techStack.contains("MySQL"));
23
24 // Remove by index vs by value
25 techStack.remove(0); // removes "Java" at index 0
26 System.out.println("After remove(0): " + techStack);
27
28 techStack.remove("Java"); // removes first occurrence of "Java" by value
29 System.out.println("After remove(Java): " + techStack);
30 }
31}Output:
Tech stack : [Java, Hibernate, Spring Boot, MySQL, Java]
Size : 5
Element at [0] : Java
Index of Java : 0
Contains MySQL : true
After remove(0): [Hibernate, Spring Boot, MySQL, Java]
After remove(Java): [Hibernate, Spring Boot, MySQL]
Set — Unique Elements
Set<E> automatically rejects duplicates. Adding an element that already exists returns false and leaves the set unchanged. No index — you cannot call get(0) on a Set.
1// File: SetBasicsDemo.java
2
3import java.util.HashSet;
4import java.util.LinkedHashSet;
5import java.util.Set;
6import java.util.TreeSet;
7
8public class SetBasicsDemo {
9
10 public static void main(String[] args) {
11
12 // HashSet — fastest, no guaranteed order
13 Set<String> uniqueTags = new HashSet<>();
14 uniqueTags.add("java");
15 uniqueTags.add("backend");
16 uniqueTags.add("java"); // duplicate — silently ignored
17 uniqueTags.add("spring");
18 uniqueTags.add("backend"); // duplicate — silently ignored
19 System.out.println("HashSet size : " + uniqueTags.size()); // 3, not 5
20 System.out.println("HashSet contents: " + uniqueTags); // order not predictable
21 System.out.println("add() duplicate : " + uniqueTags.add("java")); // returns false
22
23 System.out.println();
24
25 // LinkedHashSet — no duplicates, preserves insertion order
26 Set<String> orderedUnique = new LinkedHashSet<>();
27 orderedUnique.add("java");
28 orderedUnique.add("backend");
29 orderedUnique.add("spring");
30 orderedUnique.add("java"); // ignored
31 System.out.println("LinkedHashSet: " + orderedUnique); // insertion order preserved
32
33 System.out.println();
34
35 // TreeSet — no duplicates, sorted order
36 Set<Integer> sortedScores = new TreeSet<>();
37 sortedScores.add(85);
38 sortedScores.add(92);
39 sortedScores.add(78);
40 sortedScores.add(92); // ignored
41 sortedScores.add(65);
42 System.out.println("TreeSet (sorted): " + sortedScores); // always ascending
43 }
44}Output:
HashSet size : 3
HashSet contents: [spring, backend, java]
add() duplicate : false
LinkedHashSet: [java, backend, spring]
TreeSet (sorted): [65, 78, 85, 92]
Map — Key-Value Pairs
Map<K,V> stores associations between unique keys and values. Given a key, you can retrieve its value in O(1) average time with HashMap — far faster than searching a list.
1// File: MapBasicsDemo.java
2
3import java.util.HashMap;
4import java.util.LinkedHashMap;
5import java.util.Map;
6import java.util.TreeMap;
7
8public class MapBasicsDemo {
9
10 public static void main(String[] args) {
11
12 // HashMap — fastest, no guaranteed key order
13 Map<String, Double> productPrices = new HashMap<>();
14 productPrices.put("Laptop", 45999.0);
15 productPrices.put("Mouse", 599.0);
16 productPrices.put("Keyboard", 1299.0);
17 productPrices.put("Monitor", 12999.0);
18 productPrices.put("Laptop", 43999.0); // replaces the existing value
19
20 System.out.println("Price of Laptop : Rs." + productPrices.get("Laptop"));
21 System.out.println("Price of Webcam : " + productPrices.get("Webcam")); // null — not found
22 System.out.println("Size (unique keys): " + productPrices.size());
23 System.out.println("Contains 'Mouse' : " + productPrices.containsKey("Mouse"));
24
25 System.out.println();
26
27 // Iterate all key-value pairs
28 System.out.println("=== Product catalogue ===");
29 for (Map.Entry<String, Double> entry : productPrices.entrySet()) {
30 System.out.printf(" %-12s Rs.%.2f%n", entry.getKey(), entry.getValue());
31 }
32
33 System.out.println();
34
35 // TreeMap — keys in sorted order
36 Map<String, Integer> wordCount = new TreeMap<>();
37 wordCount.put("java", 15);
38 wordCount.put("spring", 8);
39 wordCount.put("backend", 12);
40 wordCount.put("api", 20);
41 System.out.println("TreeMap (sorted keys): " + wordCount);
42
43 // LinkedHashMap — keys in insertion order
44 Map<String, String> config = new LinkedHashMap<>();
45 config.put("host", "localhost");
46 config.put("port", "8080");
47 config.put("db", "myapp_db");
48 System.out.println("LinkedHashMap (insertion order): " + config);
49 }
50}Output:
Price of Laptop : Rs.43999.0
Price of Webcam : null
Size (unique keys): 4
Contains 'Mouse' : true
=== Product catalogue ===
Laptop Rs.43999.00
Mouse Rs.599.00
Keyboard Rs.1299.00
Monitor Rs.12999.00
TreeMap (sorted keys): {api=20, backend=12, java=15, spring=8}
LinkedHashMap (insertion order): {host=localhost, port=8080, db=myapp_db}
Queue — FIFO Processing
Queue<E> models a waiting line. Elements enter at the tail, leave from the head. ArrayDeque is the recommended default implementation.
1// File: QueueBasicsDemo.java
2
3import java.util.ArrayDeque;
4import java.util.PriorityQueue;
5import java.util.Queue;
6
7public class QueueBasicsDemo {
8
9 public static void main(String[] args) {
10
11 // ArrayDeque as Queue — FIFO order
12 Queue<String> taskQueue = new ArrayDeque<>();
13 taskQueue.offer("Send welcome email"); // adds to tail
14 taskQueue.offer("Create user profile");
15 taskQueue.offer("Assign onboarding task");
16
17 System.out.println("Queue: " + taskQueue);
18 System.out.println("peek() — next to process: " + taskQueue.peek()); // no removal
19 System.out.println("poll() — process task : " + taskQueue.poll()); // removes head
20 System.out.println("Queue after poll: " + taskQueue);
21
22 System.out.println();
23
24 // PriorityQueue — processes highest-priority first
25 Queue<Integer> urgencyQueue = new PriorityQueue<>(); // natural order: smallest first
26 urgencyQueue.offer(3); // medium priority
27 urgencyQueue.offer(1); // highest priority
28 urgencyQueue.offer(5); // lowest priority
29 urgencyQueue.offer(2);
30
31 System.out.print("Processing by priority: ");
32 while (!urgencyQueue.isEmpty()) {
33 System.out.print(urgencyQueue.poll() + " "); // always returns smallest
34 }
35 System.out.println();
36 }
37}Output:
Queue: [Send welcome email, Create user profile, Assign onboarding task]
peek() — next to process: Send welcome email
poll() — process task : Send welcome email
Queue after poll: [Create user profile, Assign onboarding task]
Processing by priority: 1 2 3 5
Internal Working of Key Implementations
Understanding how each collection stores data internally is what makes you dangerous in technical interviews. You stop guessing and start reasoning.
ARRAYLIST — Backed by Object[]
elementData: [ "A" | "B" | "C" | null | null | null | null | null | null | null ]
0 1 2 9
size=3, capacity=10
Growth: newCapacity = oldCapacity + (oldCapacity >> 1) → roughly 1.5x
HASHSET / HASHMAP — Backed by hash table (array of buckets)
table[0]: null
table[1]: Entry("Laptop", 45999) → null
table[2]: null
table[3]: Entry("Mouse", 599) → Entry("Keyboard", 1299) → null (collision chain)
...
get(key): compute hash → find bucket → compare with equals() → return value
Average O(1) for get/put/contains
TREESET / TREEMAP — Backed by Red-Black tree (self-balancing BST)
5(B)
/ \
3(R) 7(R)
/ \ / \
1(B) 4(B)6(B) 8(B)
All keys always in sorted order — in-order traversal gives sorted sequence
Guaranteed O(log n) for get/put/contains regardless of insertion order
LINKEDLIST — Backed by doubly-linked Node chain
null ← [prev|A|next] ↔ [prev|B|next] ↔ [prev|C|next] → null
↑ ↑
first last
addFirst / addLast: O(1) — update pointer only
get(index): O(n) — must traverse from head or tail
Real-World Example — Razorpay Payment Dashboard
A payment processing service tracks transactions in real time. Different features need different access patterns — order history needs a sequence, active payment methods need fast uniqueness checks, and the dashboard needs a key-based lookup of totals by merchant. Each collection type serves a distinct purpose in the same system.
1// File: PaymentRecord.java
2
3public class PaymentRecord {
4
5 private final String transactionId;
6 private final String merchant;
7 private final double amount;
8 private final String status;
9
10 public PaymentRecord(String transactionId, String merchant,
11 double amount, String status) {
12 this.transactionId = transactionId;
13 this.merchant = merchant;
14 this.amount = amount;
15 this.status = status;
16 }
17
18 public String getTransactionId() { return transactionId; }
19 public String getMerchant() { return merchant; }
20 public double getAmount() { return amount; }
21 public String getStatus() { return status; }
22
23 @Override
24 public String toString() {
25 return String.format("[%s] %-15s Rs.%7.2f %s",
26 transactionId, merchant, amount, status);
27 }
28}1// File: PaymentDashboard.java
2
3import java.util.ArrayList;
4import java.util.HashMap;
5import java.util.HashSet;
6import java.util.LinkedList;
7import java.util.List;
8import java.util.Map;
9import java.util.Queue;
10import java.util.Set;
11
12public class PaymentDashboard {
13
14 // List — all transactions in arrival order (history timeline)
15 private final List<PaymentRecord> transactionHistory = new ArrayList<>();
16
17 // Set — unique merchants who have transacted (no duplicates needed)
18 private final Set<String> activeMerchants = new HashSet<>();
19
20 // Map — total revenue collected per merchant for the dashboard widget
21 private final Map<String, Double> revenueByMerchant = new HashMap<>();
22
23 // Queue — pending transactions waiting for processing
24 private final Queue<PaymentRecord> processingQueue = new LinkedList<>();
25
26 public void recordTransaction(PaymentRecord record) {
27 transactionHistory.add(record); // preserve arrival order
28 activeMerchants.add(record.getMerchant()); // deduplication automatic
29
30 // Accumulate revenue per merchant
31 revenueByMerchant.merge(record.getMerchant(),
32 record.getAmount(), Double::sum);
33
34 if ("PENDING".equals(record.getStatus())) {
35 processingQueue.offer(record); // queue for async processing
36 }
37 }
38
39 public void processNextPending() {
40 PaymentRecord next = processingQueue.poll(); // FIFO — oldest pending first
41 if (next != null) {
42 System.out.println(" Processing: " + next);
43 } else {
44 System.out.println(" No pending transactions.");
45 }
46 }
47
48 public void printDashboard() {
49 System.out.println("╔══════════════════════════════════════════════════╗");
50 System.out.println("║ RAZORPAY PAYMENT DASHBOARD ║");
51 System.out.println("╚══════════════════════════════════════════════════╝");
52
53 System.out.println("\n--- Transaction History (ArrayList — insertion order) ---");
54 transactionHistory.forEach(t -> System.out.println(" " + t));
55
56 System.out.println("\n--- Active Merchants (HashSet — unique, fast lookup) ---");
57 System.out.println(" " + activeMerchants);
58 System.out.println(" Unique merchant count: " + activeMerchants.size());
59
60 System.out.println("\n--- Revenue by Merchant (HashMap — O(1) key lookup) ---");
61 revenueByMerchant.forEach((merchant, total) ->
62 System.out.printf(" %-20s Rs.%.2f%n", merchant, total));
63
64 System.out.println("\n--- Pending Queue (Queue — FIFO processing order) ---");
65 System.out.println(" Pending count: " + processingQueue.size());
66 }
67
68 public static void main(String[] args) {
69
70 PaymentDashboard dashboard = new PaymentDashboard();
71
72 dashboard.recordTransaction(new PaymentRecord("TXN001", "Swiggy", 1299.00, "SUCCESS"));
73 dashboard.recordTransaction(new PaymentRecord("TXN002", "Flipkart", 5499.00, "SUCCESS"));
74 dashboard.recordTransaction(new PaymentRecord("TXN003", "Swiggy", 499.00, "PENDING"));
75 dashboard.recordTransaction(new PaymentRecord("TXN004", "Zomato", 899.00, "SUCCESS"));
76 dashboard.recordTransaction(new PaymentRecord("TXN005", "Flipkart", 2999.00, "PENDING"));
77 dashboard.recordTransaction(new PaymentRecord("TXN006", "Swiggy", 749.00, "SUCCESS"));
78
79 dashboard.printDashboard();
80
81 System.out.println("\n--- Processing pending transactions (Queue.poll) ---");
82 dashboard.processNextPending(); // TXN003 — oldest pending
83 dashboard.processNextPending(); // TXN005 — next pending
84 dashboard.processNextPending(); // empty queue
85 }
86}Output:
╔══════════════════════════════════════════════════╗
║ RAZORPAY PAYMENT DASHBOARD ║
╚══════════════════════════════════════════════════╝
--- Transaction History (ArrayList — insertion order) ---
[TXN001] Swiggy Rs. 1299.00 SUCCESS
[TXN002] Flipkart Rs. 5499.00 SUCCESS
[TXN003] Swiggy Rs. 499.00 PENDING
[TXN004] Zomato Rs. 899.00 SUCCESS
[TXN005] Flipkart Rs. 2999.00 PENDING
[TXN006] Swiggy Rs. 749.00 SUCCESS
--- Active Merchants (HashSet — unique, fast lookup) ---
[Swiggy, Flipkart, Zomato]
Unique merchant count: 3
--- Revenue by Merchant (HashMap — O(1) key lookup) ---
Swiggy Rs.2547.00
Flipkart Rs.8498.00
Zomato Rs.899.00
--- Pending Queue (Queue — FIFO processing order) ---
Pending count: 2
--- Processing pending transactions (Queue.poll) ---
Processing: [TXN003] Swiggy Rs. 499.00 PENDING
Processing: [TXN005] Flipkart Rs. 2999.00 PENDING
No pending transactions.
Performance Considerations
| Collection | get / lookup | add (end) | remove | contains | Order |
|---|---|---|---|---|---|
| ArrayList | O(1) by index | O(1) amort | O(n) shift | O(n) | Insertion |
| LinkedList | O(n) traverse | O(1) head/tail | O(1) at ends | O(n) | Insertion |
| HashSet | — | O(1) avg | O(1) avg | O(1) avg | None |
| LinkedHashSet | — | O(1) avg | O(1) avg | O(1) avg | Insertion |
| TreeSet | — | O(log n) | O(log n) | O(log n) | Sorted |
| HashMap | O(1) avg by key | O(1) avg | O(1) avg | O(1) avg key | None |
| LinkedHashMap | O(1) avg by key | O(1) avg | O(1) avg | O(1) avg key | Insertion |
| TreeMap | O(log n) by key | O(log n) | O(log n) | O(log n) | Sorted |
| ArrayDeque | O(1) head/tail | O(1) amort | O(1) ends | O(n) | FIFO/LIFO |
| PriorityQueue | O(1) peek min | O(log n) | O(log n) | O(n) | Priority |
Thread safety: None of the standard implementations above are thread-safe. For concurrent use: ConcurrentHashMap (high-performance concurrent Map), CopyOnWriteArrayList (read-heavy concurrent List), Collections.synchronizedList(list) (simple single-lock wrapper).
Best Practices
Always declare the variable type as the interface, not the implementation. Write List<String> names = new ArrayList<>() rather than ArrayList<String> names = new ArrayList<>(). This lets you swap ArrayList for LinkedList or CopyOnWriteArrayList by changing one line, not every method signature. This is the first thing seniors check in code reviews.
Choose the collection based on the primary access pattern, not the data shape. A list of orders sounds like an ArrayList, but if the main operation is "is this order ID already processed?", you need a HashSet<String> for O(1) lookup. The collection serves the algorithm, not the domain model.
Use Map.getOrDefault() and Map.merge() instead of null checks. map.getOrDefault(key, 0) eliminates the if (map.get(key) == null) boilerplate. map.merge(key, 1, Integer::sum) is the idiomatic way to build a frequency counter — it handles both the first occurrence and subsequent increments in one atomic call.
Pre-size collections for bulk loads. new ArrayList<>(10_000) and new HashMap<>(10_000) prevent repeated internal array resizing when you know roughly how many elements to expect. Each resize copies all existing elements — five resizes on a 10,000-element list means copying 50,000+ element references unnecessarily.
Common Mistakes
Mistake 1 — Using List.contains() in a Loop for Large Data
1// O(n²) — contains scans the entire list on every call
2List<String> processedIds = new ArrayList<>();
3for (String id : incomingIds) {
4 if (!processedIds.contains(id)) { // O(n) per call
5 processedIds.add(id);
6 process(id);
7 }
8}
9
10// O(n) — HashSet.add() returns false for duplicates; O(1) per call
11Set<String> processedIds = new HashSet<>();
12for (String id : incomingIds) {
13 if (processedIds.add(id)) { // returns false if already present
14 process(id);
15 }
16}Mistake 2 — Modifying a List Inside a for-each Loop
1List<String> items = new ArrayList<>(List.of("A", "B", "C", "D"));
2
3// Throws ConcurrentModificationException at runtime
4for (String item : items) {
5 if (item.equals("B")) {
6 items.remove(item); // modifies list while iterator is running
7 }
8}
9
10// Correct — removeIf handles the bookkeeping internally
11items.removeIf("B"::equals);Mistake 3 — Using == Instead of equals() for Map Key Lookup
1Map<String, Integer> scores = new HashMap<>();
2scores.put("Priya", 95);
3
4String key = new String("Priya"); // different object, same content
5
6// WRONG — always false for objects; == compares references, not content
7if (key == "Priya") { ... }
8
9// CORRECT — HashMap uses equals() internally; this works as expected
10System.out.println(scores.get(key)); // 95 — equals() comparison
11System.out.println(scores.containsKey(key)); // trueMistake 4 — Iterating keySet() and Calling get() Per Key
1Map<String, Integer> map = new HashMap<>(Map.of("A", 1, "B", 2, "C", 3));
2
3// Inefficient — two lookups per entry (keySet traversal + get)
4for (String key : map.keySet()) {
5 System.out.println(key + " = " + map.get(key)); // get() is an extra O(1) call
6}
7
8// Correct — entrySet gives both key and value in a single traversal
9for (Map.Entry<String, Integer> entry : map.entrySet()) {
10 System.out.println(entry.getKey() + " = " + entry.getValue());
11}Interview Questions
Q1. What is the Java Collections Framework?
The Java Collections Framework is a set of interfaces, implementations, and algorithms in java.util for managing groups of objects. It provides standard data structures — ArrayList, HashMap, HashSet, TreeMap, ArrayDeque — that every Java application uses. The framework introduced a unified interface hierarchy: Collection → List/Set/Queue, with Map as a separate branch for key-value pairs. Before the framework existed in Java 1.2, developers built their own data structures or used the limited, inconsistent Vector and Hashtable classes.
Q2. What is the difference between List, Set, and Map?
List is an ordered sequence that allows duplicate elements and supports index-based access — ArrayList.get(0) retrieves the first element. Set is a collection that rejects duplicates — adding the same element twice has no effect and add() returns false. Map stores key-value pairs where keys are unique — HashMap.put("key", value) replaces any existing value for that key. Map does not extend Collection because its key-value structure does not fit the single-element add(E) contract. The practical decision rule: key-value lookup → Map, uniqueness required → Set, ordered sequence → List.
Q3. What is the difference between ArrayList and LinkedList?
Both implement List. ArrayList stores elements in a contiguous Object[] array — O(1) random access by index and cache-friendly sequential iteration. LinkedList stores each element in a Node with prev/next references — O(1) insertion and deletion at the head and tail, but O(n) for index-based access because there is no array to jump into. In practice, ArrayList outperforms LinkedList for the vast majority of operations due to CPU cache locality. Choose LinkedList only when insertions and removals at both ends of a large list are the primary operation.
Q4. What is the difference between HashMap and TreeMap?
HashMap stores entries in a hash table — O(1) average for get(), put(), and remove(), with no guaranteed key iteration order. TreeMap stores entries in a Red-Black tree — O(log n) for all operations, but keys are always iterated in sorted natural order. TreeMap also offers navigation methods (floorKey(), ceilingKey(), headMap(), tailMap()) that HashMap does not. Choose HashMap for pure key-value lookup; choose TreeMap when sorted key iteration or range queries are needed.
Q5. How does HashSet prevent duplicates?
HashSet is backed internally by a HashMap. When you call set.add(element), it calls map.put(element, PRESENT) on the backing map. HashMap determines key uniqueness using hashCode() first (to find the bucket) and then equals() (to find an exact match within the bucket). If the key already exists, the put returns the old value and the element is considered a duplicate. This is why any class used in a HashSet must override both hashCode() and equals() consistently — if two logically equal objects have different hash codes, they land in different buckets and the set stores both, violating uniqueness.
Q6. What is the Collections utility class and how does it differ from the Collection interface?
java.util.Collection (singular) is an interface — the root of the collection hierarchy, extended by List, Set, and Queue. It defines the contract: add(), remove(), contains(), size(), iterator(). java.util.Collections (plural) is a final utility class with only static methods — it has no instances. It provides algorithms: sort(), reverse(), shuffle(), binarySearch(), min(), max(), frequency(), unmodifiableList(), synchronizedList(). The confusion between the two names is one of the most common beginner questions and a reliable interview gotcha at service-based companies.
FAQs
What is the best collection to use for most situations in Java?
The defaults work for the vast majority of real code: use ArrayList for ordered lists, HashSet for unique element collections, and HashMap for key-value lookups. These three cover perhaps 90% of all collection usage in typical Java applications. Reach for LinkedHashMap when insertion order matters in a map, TreeMap when sorted key iteration is needed, and ConcurrentHashMap when multiple threads share a map.
What is the difference between Collection.remove() and Collections.remove()?
Collection.remove(Object o) is an instance method on any collection — it removes the first occurrence of the specified element from that collection. Collections (the utility class) does not have a remove() method at all. The utility class provides Collections.sort(), Collections.min(), Collections.unmodifiableList(), and similar algorithms. The two are unrelated beyond sharing most of their name.
Why does Map not extend Collection in Java?
Collection<E> models a group of individual elements with a single add(E element) method. Map<K,V> models associations between keys and values — every entry is inherently two things. Forcing Map to implement add(element) does not make semantic sense for a key-value store. Instead, Map forms its own parallel hierarchy. You access collection views of a map using keySet() (returns Set<K>), values() (returns Collection<V>), and entrySet() (returns Set<Map.Entry<K,V>>), each of which participates normally in the Collection hierarchy.
How do I iterate a HashMap in Java?
Three patterns: map.entrySet() to iterate both key and value together (for (Map.Entry<K,V> e : map.entrySet())), map.keySet() to iterate keys only, and map.values() to iterate values only. The entry set approach is most efficient when both key and value are needed — it avoids the extra map.get(key) call per iteration that the key set approach requires. Java 8+ adds map.forEach((k, v) -> ...) as a cleaner lambda-based alternative.
What is the difference between fail-fast and fail-safe iterators?
Fail-fast iterators — used by ArrayList, HashMap, HashSet — throw ConcurrentModificationException if the collection is structurally modified while iteration is in progress. They detect this via an internal modCount counter. Fail-safe iterators — used by CopyOnWriteArrayList and ConcurrentHashMap — do not throw. CopyOnWriteArrayList iterates a snapshot of the array taken at iterator creation time. ConcurrentHashMap uses a weakly consistent iterator that may or may not reflect concurrent modifications.
Can two equal keys exist in a HashMap?
No. HashMap guarantees key uniqueness. Two keys are considered equal if hashCode() produces the same bucket index and equals() returns true. If you put() with a key that already exists, the new value replaces the old one and put() returns the old value. This is why implementing equals() without implementing hashCode() (or vice versa) breaks HashMap — two logically equal objects with different hash codes land in different buckets and are both stored as if they were different keys.
What is the difference between Comparable and Comparator in Collections?
Comparable<T> is implemented by the class itself and defines the natural ordering through compareTo(T other). It is used automatically by Collections.sort(), TreeSet, and TreeMap when no external ordering is provided. Comparator<T> is a separate strategy class that defines an external ordering — useful when you need multiple sort orders for the same class, or when the class is not yours to modify. Java 8 made Comparator a functional interface, so it can be written as a lambda: Comparator.comparing(Product::getPrice).
Summary
The Java Collections Framework is the backbone of data management in Java. The hierarchy has two branches: Collection (List, Set, Queue) for individual elements, and Map for key-value pairs. The three-question decision process — key-value? → Map; unique? → Set; otherwise → List — points to the right collection family every time. The implementation choice follows: Hash variants for O(1) speed with no order, Linked variants for insertion order, Tree variants for sorted order.
The defaults cover most code: ArrayList, HashSet, HashMap. The specialised types — TreeMap, PriorityQueue, CopyOnWriteArrayList, ConcurrentHashMap — each solve a specific problem and should be chosen deliberately, not by default.
For interviews: know the hierarchy by heart, understand why Map does not extend Collection, be able to explain HashSet's duplicate prevention through hashCode() and equals(), and always know the time complexity of the operations you are using. These are the questions that open the rest of the collections discussion at every company from TCS to Flipkart.
What to Read Next
| Topic | Link |
|---|---|
| How ArrayList works internally with resizing, O(1) access, and fail-fast iteration | Java ArrayList → |
| How HashMap stores entries using hashing, buckets, and the equals-hashCode contract | Java HashMap → |
| How the Iterator pattern enables safe traversal and removal across all collections | Java Iterator → |
| How Generics make every collection type-safe without explicit casting | Java Generics → |
| How Java Streams process any collection with filter, map, reduce, and collect | Java Streams API → |