Java Tutorial
🔍

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.

Java
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).

Java
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.

Java
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.

Java
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.

Java
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.

Java
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}
Java
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

Collectionget / lookupadd (end)removecontainsOrder
ArrayListO(1) by indexO(1) amortO(n) shiftO(n)Insertion
LinkedListO(n) traverseO(1) head/tailO(1) at endsO(n)Insertion
HashSetO(1) avgO(1) avgO(1) avgNone
LinkedHashSetO(1) avgO(1) avgO(1) avgInsertion
TreeSetO(log n)O(log n)O(log n)Sorted
HashMapO(1) avg by keyO(1) avgO(1) avgO(1) avg keyNone
LinkedHashMapO(1) avg by keyO(1) avgO(1) avgO(1) avg keyInsertion
TreeMapO(log n) by keyO(log n)O(log n)O(log n)Sorted
ArrayDequeO(1) head/tailO(1) amortO(1) endsO(n)FIFO/LIFO
PriorityQueueO(1) peek minO(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

Java
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

Java
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

Java
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)); // true

Mistake 4 — Iterating keySet() and Calling get() Per Key

Java
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: CollectionList/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

TopicLink
How ArrayList works internally with resizing, O(1) access, and fail-fast iterationJava ArrayList →
How HashMap stores entries using hashing, buckets, and the equals-hashCode contractJava HashMap →
How the Iterator pattern enables safe traversal and removal across all collectionsJava Iterator →
How Generics make every collection type-safe without explicit castingJava Generics →
How Java Streams process any collection with filter, map, reduce, and collectJava Streams API →
Java Collections Framework | DevStackFlow