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.
// File: ArrayLimitationsDemo.java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ArrayLimitationsDemo {
public static void main(String[] args) {
System.out.println("=== Problem 1: Arrays have a fixed size ===");
// Array — must declare size at creation
String[] fixedOrders = new String[3];
fixedOrders[0] = "ORD-001";
fixedOrders[1] = "ORD-002";
fixedOrders[2] = "ORD-003";
// fixedOrders[3] = "ORD-004"; — compile error! ArrayIndexOutOfBoundsException
// ArrayList — grows automatically
List<String> orders = new ArrayList<>();
orders.add("ORD-001");
orders.add("ORD-002");
orders.add("ORD-003");
orders.add("ORD-004"); // no problem — list handles the growth
orders.add("ORD-005");
System.out.println("Array size (fixed): 3");
System.out.println("List size (dynamic): " + orders.size());
System.out.println();
System.out.println("=== Problem 2: Arrays have no built-in algorithms ===");
// Searching an array — write a manual loop
String target = "ORD-003";
boolean found = false;
for (String order : fixedOrders) {
if (target.equals(order)) { found = true; break; }
}
System.out.println("Manual array search: found=" + found);
// List — one method call
System.out.println("List contains(): found=" + orders.contains("ORD-003"));
// Sorting an array — Arrays.sort() works but produces no new API
// Sorting a List — Collections.sort() + reverse, shuffle, min, max...
Collections.sort(orders);
System.out.println("Sorted list: " + orders);
System.out.println("Min order : " + Collections.min(orders));
}
}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).
// File: ListBasicsDemo.java
import java.util.ArrayList;
import java.util.List;
public class ListBasicsDemo {
public static void main(String[] args) {
// ArrayList is the default List implementation
List<String> techStack = new ArrayList<>();
techStack.add("Java");
techStack.add("Spring Boot");
techStack.add("MySQL");
techStack.add("Java"); // duplicates allowed
techStack.add(1, "Hibernate"); // insert at index 1 — shifts others right
System.out.println("Tech stack : " + techStack);
System.out.println("Size : " + techStack.size());
System.out.println("Element at [0] : " + techStack.get(0)); // index access
System.out.println("Index of Java : " + techStack.indexOf("Java")); // first occurrence
System.out.println("Contains MySQL : " + techStack.contains("MySQL"));
// Remove by index vs by value
techStack.remove(0); // removes "Java" at index 0
System.out.println("After remove(0): " + techStack);
techStack.remove("Java"); // removes first occurrence of "Java" by value
System.out.println("After remove(Java): " + techStack);
}
}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.
// File: SetBasicsDemo.java
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
public class SetBasicsDemo {
public static void main(String[] args) {
// HashSet — fastest, no guaranteed order
Set<String> uniqueTags = new HashSet<>();
uniqueTags.add("java");
uniqueTags.add("backend");
uniqueTags.add("java"); // duplicate — silently ignored
uniqueTags.add("spring");
uniqueTags.add("backend"); // duplicate — silently ignored
System.out.println("HashSet size : " + uniqueTags.size()); // 3, not 5
System.out.println("HashSet contents: " + uniqueTags); // order not predictable
System.out.println("add() duplicate : " + uniqueTags.add("java")); // returns false
System.out.println();
// LinkedHashSet — no duplicates, preserves insertion order
Set<String> orderedUnique = new LinkedHashSet<>();
orderedUnique.add("java");
orderedUnique.add("backend");
orderedUnique.add("spring");
orderedUnique.add("java"); // ignored
System.out.println("LinkedHashSet: " + orderedUnique); // insertion order preserved
System.out.println();
// TreeSet — no duplicates, sorted order
Set<Integer> sortedScores = new TreeSet<>();
sortedScores.add(85);
sortedScores.add(92);
sortedScores.add(78);
sortedScores.add(92); // ignored
sortedScores.add(65);
System.out.println("TreeSet (sorted): " + sortedScores); // always ascending
}
}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.
// File: MapBasicsDemo.java
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
public class MapBasicsDemo {
public static void main(String[] args) {
// HashMap — fastest, no guaranteed key order
Map<String, Double> productPrices = new HashMap<>();
productPrices.put("Laptop", 45999.0);
productPrices.put("Mouse", 599.0);
productPrices.put("Keyboard", 1299.0);
productPrices.put("Monitor", 12999.0);
productPrices.put("Laptop", 43999.0); // replaces the existing value
System.out.println("Price of Laptop : Rs." + productPrices.get("Laptop"));
System.out.println("Price of Webcam : " + productPrices.get("Webcam")); // null — not found
System.out.println("Size (unique keys): " + productPrices.size());
System.out.println("Contains 'Mouse' : " + productPrices.containsKey("Mouse"));
System.out.println();
// Iterate all key-value pairs
System.out.println("=== Product catalogue ===");
for (Map.Entry<String, Double> entry : productPrices.entrySet()) {
System.out.printf(" %-12s Rs.%.2f%n", entry.getKey(), entry.getValue());
}
System.out.println();
// TreeMap — keys in sorted order
Map<String, Integer> wordCount = new TreeMap<>();
wordCount.put("java", 15);
wordCount.put("spring", 8);
wordCount.put("backend", 12);
wordCount.put("api", 20);
System.out.println("TreeMap (sorted keys): " + wordCount);
// LinkedHashMap — keys in insertion order
Map<String, String> config = new LinkedHashMap<>();
config.put("host", "localhost");
config.put("port", "8080");
config.put("db", "myapp_db");
System.out.println("LinkedHashMap (insertion order): " + config);
}
}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.
// File: QueueBasicsDemo.java
import java.util.ArrayDeque;
import java.util.PriorityQueue;
import java.util.Queue;
public class QueueBasicsDemo {
public static void main(String[] args) {
// ArrayDeque as Queue — FIFO order
Queue<String> taskQueue = new ArrayDeque<>();
taskQueue.offer("Send welcome email"); // adds to tail
taskQueue.offer("Create user profile");
taskQueue.offer("Assign onboarding task");
System.out.println("Queue: " + taskQueue);
System.out.println("peek() — next to process: " + taskQueue.peek()); // no removal
System.out.println("poll() — process task : " + taskQueue.poll()); // removes head
System.out.println("Queue after poll: " + taskQueue);
System.out.println();
// PriorityQueue — processes highest-priority first
Queue<Integer> urgencyQueue = new PriorityQueue<>(); // natural order: smallest first
urgencyQueue.offer(3); // medium priority
urgencyQueue.offer(1); // highest priority
urgencyQueue.offer(5); // lowest priority
urgencyQueue.offer(2);
System.out.print("Processing by priority: ");
while (!urgencyQueue.isEmpty()) {
System.out.print(urgencyQueue.poll() + " "); // always returns smallest
}
System.out.println();
}
}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
Basic Example
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Task 1");
list.add("Task 1"); // duplicates allowed
Set<String> set = new HashSet<>();
set.add("Task 1");
set.add("Task 1"); // duplicate ignored
Map<String, Integer> map = new HashMap<>();
map.put("Task 1", 1);
Queue<String> queue = new ArrayDeque<>();
queue.offer("Task 1");
queue.offer("Task 2");
System.out.println("List size: " + list.size());
System.out.println("Set size: " + set.size());
System.out.println("Map lookup: " + map.get("Task 1"));
System.out.println("Queue processes first: " + queue.poll());
}
}Output
List size: 2 Set size: 1 Map lookup: 1 Queue processes first: Task 1
How it works:
- ›
Listkeeps every element you add, including duplicates — its size grows to 2 even though"Task 1"was added twice. - ›
Setsilently rejects the second"Task 1"— its size stays at 1, since uniqueness is its defining contract. - ›
Mapassociates a key with a value for O(1) average lookup —get("Task 1")returns its value directly instead of scanning. - ›
Queueprocesses elements in the order they arrived —poll()returns"Task 1"first because it was offered first (FIFO). - ›Each of the four families —
List,Set,Map,Queue— solves a different problem; picking the right one is the first decision the Collections Framework asks you to make.
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
// O(n²) — contains scans the entire list on every call
List<String> processedIds = new ArrayList<>();
for (String id : incomingIds) {
if (!processedIds.contains(id)) { // O(n) per call
processedIds.add(id);
process(id);
}
}
// O(n) — HashSet.add() returns false for duplicates; O(1) per call
Set<String> processedIds = new HashSet<>();
for (String id : incomingIds) {
if (processedIds.add(id)) { // returns false if already present
process(id);
}
}Mistake 2 — Modifying a List Inside a for-each Loop
List<String> items = new ArrayList<>(List.of("A", "B", "C", "D"));
// Throws ConcurrentModificationException at runtime
for (String item : items) {
if (item.equals("B")) {
items.remove(item); // modifies list while iterator is running
}
}
// Correct — removeIf handles the bookkeeping internally
items.removeIf("B"::equals);Mistake 3 — Using == Instead of equals() for Map Key Lookup
Map<String, Integer> scores = new HashMap<>();
scores.put("Priya", 95);
String key = new String("Priya"); // different object, same content
// WRONG — always false for objects; == compares references, not content
if (key == "Priya") { ... }
// CORRECT — HashMap uses equals() internally; this works as expected
System.out.println(scores.get(key)); // 95 — equals() comparison
System.out.println(scores.containsKey(key)); // trueMistake 4 — Iterating keySet() and Calling get() Per Key
Map<String, Integer> map = new HashMap<>(Map.of("A", 1, "B", 2, "C", 3));
// Inefficient — two lookups per entry (keySet traversal + get)
for (String key : map.keySet()) {
System.out.println(key + " = " + map.get(key)); // get() is an extra O(1) call
}
// Correct — entrySet gives both key and value in a single traversal
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}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
See how every collection type fits together.
Practice Programs
Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.
Creates, modifies, and iterates over an ArrayList.
Stores key-value pairs in a HashMap and looks them up by key.
Adds elements to a HashSet and shows that duplicates are silently ignored.
Uses the Queue interface's offer/poll/peek methods to process elements first-in-first-out.
Uses the legacy Stack class for last-in-first-out access.