What is the Collections Framework?
The Java Collections Framework is a unified set of interfaces and implementations for storing, retrieving, and manipulating groups of objects. It ships with the JDK โ no dependencies needed โ and covers virtually every data structure you'll encounter in production: dynamic lists, unique sets, key-value maps, queues, and deques.
The problem it solves: arrays are fixed-size and offer almost no built-in
operations. Before the Collections Framework (pre-Java 2), every developer
wrote their own resizable list or hash table โ incompatible with everyone
else's. The framework provides a common API so that any code accepting a
List works with ArrayList, LinkedList,
or any other implementation, without changes.
// Arrays: fixed size, no built-in operations
String[] names = new String[3];
names[0] = "Alice";
// Need a 4th name? Allocate a new array, copy everything. No remove, no contains.
// Collections: dynamic, rich API, type-safe
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.remove("Alice");
names.contains("Bob"); // true
names.size(); // 1 โ grows and shrinks automatically
/*
* The Collections hierarchy:
*
* Iterable
* โโโ Collection
* โโโ List โ ordered, allows duplicates
* โ โโโ ArrayList O(1) get, O(n) insert/delete middle
* โ โโโ LinkedList O(1) insert/delete head/tail, O(n) get
* โ
* โโโ Set โ no duplicates
* โ โโโ HashSet O(1) add/contains, no order
* โ โโโ LinkedHashSet O(1) add/contains, insertion order
* โ โโโ TreeSet O(log n) add/contains, sorted
* โ
* โโโ Queue โ ordered for processing
* โโโ LinkedList FIFO queue
* โโโ ArrayDeque fast deque, prefer over LinkedList
* โโโ PriorityQueue min-heap, poll() returns smallest
*
* Map (NOT a Collection, but part of the framework)
* โโโ HashMap O(1) get/put, no order
* โโโ LinkedHashMap O(1) get/put, insertion order
* โโโ TreeMap O(log n) get/put, sorted by key
*/
// โ Locks you to ArrayList โ hard to swap implementation later
ArrayList<String> names = new ArrayList<>();
// โ
Depends on the contract โ swap implementation without touching callers
List<String> names = new ArrayList<>();
List<String> names = new LinkedList<>(); // one word change
List<String> names = new CopyOnWriteArrayList<>(); // thread-safe, one word change
List โ Ordered, Allows Duplicates
A List is the most-used collection type. It maintains insertion
order, allows duplicates, and provides index-based access. Use it as your
default unless you have a specific reason for another type.
ArrayList vs LinkedList
| Operation | ArrayList | LinkedList |
|---|---|---|
get(i) |
O(1) โ | O(n) โ |
add(e) at end |
O(1) amortised | O(1) |
add(i, e) middle |
O(n) โ shifts elements | O(n) โ traversal to index |
remove(i) middle |
O(n) โ shifts elements | O(n) โ traversal to index |
| Memory | Compact (contiguous array) | Higher (node + 2 pointers per element) |
| Default choice | โ Yes โ 90% of cases | Only for queue/deque operations |
List<String> users = new ArrayList<>();
// Add
users.add("Alice");
users.add("Bob");
users.add(0, "Zara"); // insert at index 0
// Read
users.get(0); // "Zara"
users.size(); // 3
users.contains("Bob"); // true
users.indexOf("Alice"); // 1
// Update
users.set(1, "Anna"); // replaces "Alice" with "Anna"
// Remove
users.remove("Bob"); // by value
users.remove(0); // by index โ careful: remove(0) vs remove(Integer.valueOf(0))
// Iterate
for (String u : users) { System.out.println(u); }
users.forEach(System.out::println); // Java 8+
// Sort
users.sort(Comparator.naturalOrder());
users.sort(Comparator.comparing(String::length));
// Immutable list (Java 9+) โ no add/remove allowed
List<String> fixed = List.of("A", "B", "C");
Set โ No Duplicates
A Set guarantees uniqueness โ adding the same element twice has
no effect. Use it when you need to eliminate duplicates or test membership
efficiently.
| Implementation | Order | Performance | Use when |
|---|---|---|---|
HashSet |
None | O(1) add/contains | Order irrelevant โ default choice |
LinkedHashSet |
Insertion order | O(1) add/contains | Need unique + predictable iteration |
TreeSet |
Sorted (natural/comparator) | O(log n) add/contains | Need sorted unique elements |
// Deduplication: convert List โ Set
List<String> withDupes = List.of("a", "b", "a", "c");
Set<String> unique = new HashSet<>(withDupes); // {a, b, c}
// Fast membership test โ use Set, not List, for contains() at scale
Set<String> allowedRoles = Set.of("ADMIN", "EDITOR", "VIEWER");
if (allowedRoles.contains(user.getRole())) { ... }
// List.contains() is O(n). Set.contains() is O(1). Huge difference at scale.
// TreeSet: sorted, supports range operations
TreeSet<Integer> scores = new TreeSet<>(Set.of(50, 80, 30, 90, 70));
scores.first(); // 30
scores.last(); // 90
scores.headSet(70); // {30, 50} โ elements < 70
scores.tailSet(70); // {70, 80, 90}
scores.subSet(50, 85); // {50, 70, 80}
// Immutable set (Java 9+)
Set<String> fixed = Set.of("A", "B", "C");
HashSet uses hashCode() to find the bucket and equals()
to confirm identity. If you store custom objects without overriding both,
two logically equal objects will appear as duplicates and
contains() will return false even for an element
you just added. With record (Java 16+) both are generated
automatically.
Map โ Key-Value Pairs
Map is not a Collection but is central to the
framework. It maps unique keys to values โ like a dictionary. Keys must be
unique; values can repeat. null as a value is allowed in
HashMap; as a key, only in HashMap (one null key).
Map<String, Integer> scores = new HashMap<>();
// Put / update
scores.put("Alice", 95);
scores.put("Bob", 82);
scores.put("Alice", 97); // overwrites 95
// Get
scores.get("Alice"); // 97
scores.get("Unknown"); // null โ missing key
scores.getOrDefault("Unknown", 0); // 0 โ safe fallback
// Check
scores.containsKey("Bob"); // true
scores.containsValue(82); // true
scores.size(); // 2
// Iterate
for (Map.Entry<String, Integer> e : scores.entrySet()) {
System.out.println(e.getKey() + " โ " + e.getValue());
}
scores.forEach((k, v) -> System.out.println(k + " โ " + v)); // Java 8+
// Powerful Map operations (Java 8+)
scores.putIfAbsent("Carol", 88); // only if key absent
scores.merge("Alice", 5, Integer::sum); // 97 + 5 = 102
scores.computeIfAbsent("Dan", k -> fetchScore(k)); // compute and store if absent
scores.replaceAll((k, v) -> v + 10); // bonus for everyone
// Word frequency โ classic Map pattern
Map<String, Long> freq = new HashMap<>();
for (String word : words) {
freq.merge(word, 1L, Long::sum); // cleaner than getOrDefault pattern
}
// Immutable map (Java 9+)
Map<String, Integer> fixed = Map.of("A", 1, "B", 2);
Queue and Deque
A Queue processes elements in order โ typically FIFO. A
Deque (double-ended queue) supports insertion and removal at
both ends. Prefer ArrayDeque over LinkedList
for both โ it's faster and uses less memory.
// Queue โ FIFO processing (task queues, BFS)
Queue<String> queue = new ArrayDeque<>();
queue.offer("first"); // add to tail (prefer offer over add โ no exception on full)
queue.offer("second");
queue.peek(); // "first" โ look without removing
queue.poll(); // "first" โ remove from head (returns null if empty, unlike remove())
// Deque โ stack (LIFO) or double-ended queue
Deque<String> deque = new ArrayDeque<>();
deque.push("a"); // push to head (stack behaviour)
deque.push("b");
deque.pop(); // "b" โ LIFO
// PriorityQueue โ min-heap: poll() always returns the smallest element
Queue<Integer> pq = new PriorityQueue<>();
pq.offer(5); pq.offer(1); pq.offer(3);
pq.poll(); // 1 โ always the minimum
pq.poll(); // 3
// Max-heap: reverse comparator
Queue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
Choosing the Right Collection
| Need | Use | Why |
|---|---|---|
| Ordered list with duplicates | ArrayList |
Default list. O(1) random access. |
| Frequent head/tail add/remove | ArrayDeque |
Faster than LinkedList for deque ops. |
| Unique elements, fast lookup | HashSet |
O(1) contains(). Order irrelevant. |
| Unique elements, sorted | TreeSet |
O(log n). Supports range queries. |
| Key โ value lookup | HashMap |
O(1) get/put. Default map. |
| Key โ value, sorted keys | TreeMap |
O(log n). floorKey/ceilingKey. |
| Key โ value, insertion order | LinkedHashMap |
LRU cache pattern. Predictable iteration. |
| Priority processing | PriorityQueue |
poll() always returns min (or max). |
| Thread-safe reads, rare writes | CopyOnWriteArrayList |
Concurrent reads without locking. |
| Thread-safe map | ConcurrentHashMap |
Lock striping โ much faster than Hashtable. |
Common Pitfalls
// โ Modifying a collection while iterating it with for-each
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
if (s.equals("b")) list.remove(s); // ConcurrentModificationException!
}
// โ
Option 1: Iterator.remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("b")) it.remove(); // safe
}
// โ
Option 2: removeIf (Java 8+) โ cleanest
list.removeIf(s -> s.equals("b"));
List<String> fixed = List.of("a", "b");
fixed.add("c"); // UnsupportedOperationException โ not just unmodifiable, immutable
// โ
If you need a mutable copy:
List<String> mutable = new ArrayList<>(List.of("a", "b"));
// โ Two threads writing to HashMap simultaneously โ data corruption, infinite loops
Map<String, Integer> map = new HashMap<>(); // NOT thread-safe
// โ
For concurrent access:
Map<String, Integer> map = new ConcurrentHashMap<>();
// โ
For read-heavy, rare writes:
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());
Senior Topics: Performance and Internals
HashMap internals โ why initial capacity matters
// HashMap internally uses an array of buckets.
// Default capacity: 16. Load factor: 0.75 โ resizes at 12 entries.
// Resize = new array (2x size) + rehash all entries = O(n) operation.
// โ If you know you'll store ~1000 entries, default capacity causes ~6 resizes
Map<String, User> cache = new HashMap<>();
// โ
Set initial capacity to avoid resizes: size / loadFactor + 1
Map<String, User> cache = new HashMap<>(1334); // 1000 / 0.75 โ 1334
// Java 8+: buckets with 8+ entries convert from linked list to red-black tree
// โ worst-case O(n) degrades to O(log n) even with hash collisions
LinkedHashMap as LRU cache
// LinkedHashMap with accessOrder=true moves accessed entries to tail.
// Override removeEldestEntry() to evict when size exceeds limit.
int MAX = 100;
Map<String, User> lruCache = new LinkedHashMap<>(MAX, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, User> eldest) {
return size() > MAX; // evict oldest-accessed when over limit
}
};
// Production: prefer Caffeine or Guava Cache for proper LRU with TTL support
Collections with Stream API
List<User> users = getUsers();
// Group by department โ Map<String, List<User>>
Map<String, List<User>> byDept = users.stream()
.collect(Collectors.groupingBy(User::getDepartment));
// Count per department โ Map<String, Long>
Map<String, Long> countByDept = users.stream()
.collect(Collectors.groupingBy(User::getDepartment, Collectors.counting()));
// Partition into active/inactive โ Map<Boolean, List<User>>
Map<Boolean, List<User>> partitioned = users.stream()
.collect(Collectors.partitioningBy(User::isActive));
// Collect to unmodifiable list (Java 16+)
List<String> names = users.stream().map(User::getName).toList();
Interview Questions
Q: What is the difference between ArrayList and LinkedList?
ArrayList is backed by an array โ O(1) random access via index, O(n) insert/delete
in the middle. LinkedList is a doubly-linked list โ O(1) insert/delete at
head/tail, O(n) random access. In practice, ArrayList wins for most use cases
because cache locality makes it faster even for operations where LinkedList
is theoretically equal. Use LinkedList only when you truly need a deque.
Q: What is the difference between HashMap and HashSet?
HashSet is internally backed by a HashMap where each
element is a key and the value is a dummy object. Both use hashing for O(1)
average operations. HashMap stores key-value pairs;
HashSet stores only keys (unique values).
Q: When would you use a Set over a List?
When you need uniqueness or fast membership testing.
List.contains() is O(n) โ it scans every element.
HashSet.contains() is O(1). For a list of allowed roles,
blocked IPs, or processed event IDs, a Set is dramatically more efficient
at scale.
Q: How does HashMap handle hash collisions?
Entries with the same bucket index (same hashCode() % capacity)
are stored in a linked list within that bucket. Since Java 8, when a bucket
exceeds 8 entries, it converts from a linked list to a red-black tree โ
degrading from O(n) worst-case to O(log n). If all keys hash to the same
bucket (e.g. all return hashCode() = 0), performance collapses
to O(n) for get/put. This is an actual attack vector โ HashDoS โ and why
Java uses randomised hash seeds for String keys since Java 7.
Q: What is the contract between equals() and hashCode()?
If a.equals(b) is true, then a.hashCode() == b.hashCode()
must also be true. The reverse is not required โ two objects can share a hash
code without being equal (collision). Violating this contract breaks all
hash-based collections: HashMap, HashSet,
Hashtable. A common symptom: you put an object in a HashMap,
mutate one of its fields (which changes its hash), and can no longer find it.
Q: Why is ConcurrentHashMap preferred over synchronizedMap?
Collections.synchronizedMap() wraps every method with a single
lock on the whole map โ all operations are serialised. Under contention,
this becomes a bottleneck. ConcurrentHashMap uses lock striping
(one lock per segment/bucket group), allowing concurrent reads and concurrent
writes to different segments. It also provides atomic compound operations:
putIfAbsent(), computeIfAbsent(), merge()
โ which synchronizedMap cannot guarantee atomically without
external synchronization.