Java Collections Framework — Complete Notes
Every list, set, map and queue in Java, explained from the ground up in plain language — then
taken all the way down to the internals interviewers actually dig into (especially how
HashMap really works).
00. What the Collections Framework actually is
A collection is just "a bunch of objects grouped together." The Collections Framework is Java's built-in, standardised toolbox of these groupings, so you never have to hand-roll a resizable array or a hash table again.
Before the framework existed (Java 1.1), everyone used raw arrays, Vector and
Hashtable, and none of them shared a common interface — so code that worked with one
couldn't work with another. Java 1.2 introduced a unified design: a small set of
interfaces (what a collection can do), many implementations (how
it does it), and a couple of utility classes (algorithms like sort and search).
Learn the interfaces once and every implementation feels familiar.
Think of the interfaces as job descriptions ("a List must keep order and allow
duplicates") and the implementations as different employees who can do that job
in their own way (ArrayList is fast at random access, LinkedList is
fast at inserting in the middle). You write your code against the job description, then hire
whichever employee fits your workload — and you can swap them later without rewriting anything.
Two separate family trees. Collection covers things that hold
single elements — List, Set, Queue.
Map is a separate tree for key → value pairs and
does not extend Collection. People say "collections" loosely to
mean both, but in the type system they're two trees.
01. The hierarchy — how the pieces fit
Get this map in your head and everything else slots into place. Everything below
Collection shares the same core methods; only the rules differ.
Iterable
└── Collection ............. add, remove, size, contains, iterator
├── List ............. ordered, index-based, allows duplicates
│ ├── ArrayList
│ ├── LinkedList
│ └── Vector (legacy) → Stack (legacy)
├── Set .............. no duplicates
│ ├── HashSet
│ │ └── LinkedHashSet
│ └── TreeSet (implements SortedSet/NavigableSet)
└── Queue ............ ordered for processing (FIFO-ish)
├── ArrayDeque (also a Deque)
├── PriorityQueue
└── LinkedList (also a Queue/Deque)
Map (SEPARATE tree) ......... key → value, no duplicate keys
├── HashMap
│ └── LinkedHashMap
├── TreeMap (implements SortedMap/NavigableMap)
└── Hashtable (legacy)
Iterable sits at the very top — it's the interface that lets a type be used in a
for-each loop. Collection adds the bulk operations (add,
remove, contains, size). Below that, each interface
narrows the contract:
-
List— keeps insertion order, indexes elements from 0, allows duplicates. -
Set— refuses duplicates (defined byequals). -
Queue— models "a line to be processed," usually first-in-first-out. -
Deque— a double-ended queue: add/remove at both ends. -
Map— stores pairs; look up a value by its key.
Vector, Stack and Hashtable are from Java 1.0. They still
work but are synchronized on every method (slow) and generally replaced by
ArrayList, ArrayDeque and HashMap. Know they exist for
interviews; avoid them in new code.
02. List — ArrayList vs LinkedList
A List is an ordered, index-based sequence that allows duplicates. Two implementations matter, and the whole interview is "which one, and why."
ArrayList — a growable array
Internally it's a plain array (Object[]). Because it's contiguous memory, reading
list.get(i) is a single jump — O(1). When it fills up, it
grows by ~50%: it allocates a bigger array and copies everything over. That copy
is occasional, so appending is amortised O(1).
An ArrayList is a numbered row of lockers. Jumping to locker #457
is instant. But inserting a new locker in the middle means shifting every locker after
it down one — slow.
List<String> fruits = new ArrayList<>();
fruits.add("apple"); // append — O(1) amortised
fruits.add("banana");
fruits.add(1, "mango"); // insert at index 1 — shifts the rest, O(n)
fruits.get(0); // "apple" — O(1) random access
fruits.set(0, "avocado"); // replace — O(1)
fruits.remove("banana"); // remove by value — O(n) to find + shift
fruits.size(); // 2
System.out.println(fruits); // [avocado, mango]
LinkedList — a doubly-linked chain of nodes
Each element is a node holding the value plus pointers to the previous and
next node. There's no array, so there's no shifting: inserting or deleting once you're
at a node is O(1). But there's no index math either — to reach element
#457 you must walk 457 links, so get(i) is O(n).
| Operation | ArrayList | LinkedList |
|---|---|---|
get(i) random access |
O(1) | O(n) |
| append at end | O(1)* | O(1) |
| insert / remove at front | O(n) | O(1) |
| insert / remove in middle | O(n) | O(n) to find, O(1) to unlink |
| memory per element | low (just the value) | high (value + 2 pointers) |
| cache friendliness | excellent (contiguous) | poor (scattered nodes) |
Use ArrayList ~95% of the time. Modern CPUs love contiguous
memory, so even "middle inserts" are often faster on an ArrayList than a
LinkedList because walking the linked nodes thrashes the cache.
LinkedList's real niche is when you use it as a Queue/Deque
(constant-time add/remove at both ends). Default to ArrayList and only switch with
a measured reason.
If you know roughly how many elements you'll add, pre-size it:
new ArrayList<>(10_000). That skips the repeated grow-and-copy cycles and can
be a real speed-up in hot loops.
03. Set — HashSet, LinkedHashSet, TreeSet
A Set is a collection with no duplicates. "Duplicate" means
a.equals(b) is true — which is exactly why equals/hashCode
matter so much (covered in the OOP notes).
| Implementation | Ordering | Backed by | Speed (add/contains) |
|---|---|---|---|
HashSet |
none (unpredictable) | a HashMap |
O(1) average |
LinkedHashSet |
insertion order | HashMap + linked list |
O(1) average |
TreeSet |
sorted order | a red-black tree | O(log n) |
Set<Integer> hash = new HashSet<>(List.of(3, 1, 2, 1));
Set<Integer> linked = new LinkedHashSet<>(List.of(3, 1, 2, 1));
Set<Integer> tree = new TreeSet<>(List.of(3, 1, 2, 1));
System.out.println(hash); // [1, 2, 3] — no guaranteed order (looks sorted here by luck)
System.out.println(linked); // [3, 1, 2] — the order you inserted
System.out.println(tree); // [1, 2, 3] — always sorted ascending
HashSet when you just need uniqueness and don't care about order (the default).
LinkedHashSet when you want uniqueness and to remember insertion order
(e.g. de-duplicating a list while preserving sequence). TreeSet when you need the
elements kept sorted, or need range queries like "give me everything ≥ 10"
(tailSet, ceiling, floor).
TreeSet needs ordering
Its elements must be Comparable, or you must hand it a Comparator. Put
non-comparable objects in without one and you get a ClassCastException at runtime.
Also, TreeSet decides "duplicate" using compareTo == 0, not
equals — a subtle trap.
04. Map — and how HashMap really works
A Map stores key → value pairs with unique keys. This is
the single most-asked Java topic after OOP, because HashMap's internals touch
hashing, equals/hashCode, and data structures all at once.
Map<String, Integer> ages = new HashMap<>();
ages.put("Asha", 30);
ages.put("Ravi", 25);
ages.put("Asha", 31); // same key → OVERWRITES, not duplicates. Now Asha = 31
ages.get("Asha"); // 31
ages.getOrDefault("Zoe", 0); // 0 — key absent, returns the default
ages.containsKey("Ravi"); // true
ages.putIfAbsent("Ravi", 99); // no-op, Ravi already present
// Idiomatic counting / grouping:
Map<String, Integer> count = new HashMap<>();
for (String w : words) count.merge(w, 1, Integer::sum); // +1 per word
count.computeIfAbsent("list", k -> new ArrayList<>()); // create-on-first-use pattern
for (Map.Entry<String, Integer> e : ages.entrySet())
System.out.println(e.getKey() + " → " + e.getValue());
Inside HashMap — the part interviewers love
Imagine a wall of numbered pigeonholes (an array called the table, made of "buckets"). To file a key, you compute a number from it (its hash), map that number to a pigeonhole, and drop the entry in. To find it later, you recompute the same number and go straight to that pigeonhole — no scanning the whole wall. That direct jump is why lookups are O(1) on average.
Step by step: what put(key, value) does
- Call
key.hashCode()to get a raw 32-bit number. -
Spread the bits (Java does
h ^ (h >>> 16)) so that even keys with similar hashes land in different buckets. This mixing reduces collisions. -
Map that to a bucket index with
hash & (n - 1), wherenis the table length. (This works as a fast modulo because the table length is always a power of two — that's why it is.) -
Go to that bucket. If it's empty, place the entry. If something's already there — a
collision — walk the entries in that bucket comparing with
hashCodethenequals. If a key equals an existing one, overwrite its value; otherwise append the new entry.
hashCode AND equals
hashCode picks the bucket (fast, narrows a million entries to a handful).
equals confirms the exact key within that bucket (correct). You need both:
hashCode for speed, equals for correctness. Break the contract — equal objects with different
hashCodes — and your key gets filed in one bucket but looked up in another, so it "vanishes."
This is the single most important reason the
equals/hashCode contract exists.
Collisions: linked list, then tree
When many keys land in the same bucket, they're chained together. Historically that chain was a
linked list, so a badly-collided bucket degraded lookups to
O(n). Since Java 8, once a single bucket holds
8+ entries (TREEIFY_THRESHOLD) and the table is at least 64
slots, that bucket is converted into a balanced red-black tree, bringing
worst-case lookup back down to O(log n). If the bucket later shrinks below 6, it
turns back into a list.
Resizing (rehashing)
The map tracks a load factor (default 0.75). When the
number of entries exceeds capacity × loadFactor — e.g. 12 entries in a 16-slot table
— it doubles the table size and redistributes every entry into the new, larger
table. This keeps buckets short. Resizing is O(n) when it happens, but it's rare, so
put/get stay amortised O(1).
| Constant / default | Value | Meaning |
|---|---|---|
| initial capacity | 16 | starting number of buckets |
| load factor | 0.75 | fill fraction that triggers a resize |
| resize multiplier | ×2 | capacity always doubles (stays a power of two) |
| treeify threshold | 8 | bucket becomes a tree at 8+ entries |
| untreeify threshold | 6 | tree reverts to a list below 6 |
| min treeify capacity | 64 | below this it resizes instead of treeifying |
If you use an object as a key and then mutate a field that affects its hashCode,
the map computes a new bucket on lookup but the entry is still filed under the
old one — so get returns null even though the entry is right
there. Keys should be immutable (this is why String and the wrapper types make
ideal keys).
The other Maps
-
LinkedHashMap— aHashMapthat also threads a linked list through its entries to remember insertion order (or, with a flag, access order — the basis of a simple LRU cache). -
TreeMap— keeps keys sorted (red-black tree),O(log n)operations, and adds navigation likefirstKey,floorKey,headMap,subMap. -
Hashtable— legacy, fully synchronized, no null keys/values. For thread-safety today useConcurrentHashMapinstead.
HashMap allows one null key and many null values.
TreeMap allows null values but not null keys (it can't sort
null). ConcurrentHashMap and Hashtable allow
no nulls at all.
05. Queue, Deque & PriorityQueue
A Queue models a line of items waiting to be processed. A Deque ("deck") is a double-ended queue — push and pop at both ends, so it can act as either a queue (FIFO) or a stack (LIFO).
Deque<Integer> dq = new ArrayDeque<>();
// As a FIFO queue:
dq.offer(1); dq.offer(2); // add to tail
dq.poll(); // 1 — remove from head
// As a LIFO stack (use this, NOT the legacy Stack class):
dq.push(10); dq.push(20); // add to head
dq.pop(); // 20 — remove from head
dq.peek(); // 10 — look without removing
ArrayDeque, not Stack
The old Stack class extends Vector and is synchronized and slow. For
stack or queue behaviour, ArrayDeque is the modern, faster choice.
PriorityQueue — a heap
A PriorityQueue doesn't return items in insertion order — it returns the
smallest (or by your Comparator, the "highest priority") first. It's
backed by a binary heap: offer and poll are
O(log n), peeking at the top is O(1). It's the go-to for "top-K,"
scheduling, and Dijkstra-style algorithms.
PriorityQueue<Integer> pq = new PriorityQueue<>(); // min-heap by default
pq.offer(5); pq.offer(1); pq.offer(3);
pq.poll(); // 1 (smallest first)
pq.poll(); // 3
// Max-heap: reverse the comparator
PriorityQueue<Integer> max = new PriorityQueue<>(Comparator.reverseOrder());
06. Iterating — and the fail-fast trap
You'll loop over collections constantly. The one thing that bites everyone is modifying a collection while iterating it.
List<String> names = new ArrayList<>(List.of("a", "b", "c"));
for (String n : names) { ... } // for-each (uses an Iterator under the hood)
Iterator<String> it = names.iterator(); // explicit iterator
while (it.hasNext()) {
String n = it.next();
if (n.equals("b")) it.remove(); // SAFE removal during iteration
}
names.removeIf(n -> n.startsWith("a")); // cleanest conditional removal (Java 8+)
If you add or remove from a collection through the collection itself while a for-each
loop is running over it, most implementations throw
ConcurrentModificationException. They're fail-fast: they keep a
modCount and notice the structure changed underneath the iterator. The fix is to
mutate through the iterator (it.remove()) or use removeIf.
Despite the name, this happens in ordinary single-threaded code — it has nothing to do with
threads.
07. Comparable vs Comparator — sorting
To sort objects, Java needs to know what "less than" means. Two mechanisms: build the ordering
into the class (Comparable), or supply it from outside
(Comparator).
-
Comparable<T>— implemented by the class itself; defines its one natural ordering viacompareTo. Example:Stringsorts alphabetically,Integernumerically. -
Comparator<T>— a separate object defining an ordering. You can make many (by name, by age, reversed) without touching the class.
class Person implements Comparable<Person> {
String name; int age;
// natural ordering: by age
public int compareTo(Person o) { return Integer.compare(this.age, o.age); }
}
List<Person> people = ...;
Collections.sort(people); // uses compareTo (by age)
// External comparators — compose them fluently:
people.sort(Comparator.comparing(p -> p.name)); // by name
people.sort(Comparator.comparingInt((Person p) -> p.age).reversed()); // oldest first
people.sort(Comparator.comparing((Person p) -> p.name)
.thenComparingInt(p -> p.age)); // name, then age
compareTo/compare returns a negative number if
this is "less," zero if equal, positive if
"greater." Keep it consistent with equals where possible, and never implement it as
a - b on ints that could overflow — use Integer.compare(a, b).
08. Collections & Arrays utility classes
Two helper classes carry the algorithms. Collections (with an "s") operates on
collections; Arrays operates on arrays.
Collections.sort(list);
Collections.reverse(list);
Collections.max(list); Collections.min(list);
Collections.frequency(list, x);
Collections.unmodifiableList(list); // read-only VIEW (throws on modify)
Collections.synchronizedList(list); // thread-safe wrapper (coarse locking)
int[] arr = {3, 1, 2};
Arrays.sort(arr);
Arrays.toString(arr); // "[1, 2, 3]"
List<Integer> view = Arrays.asList(1, 2, 3); // FIXED-SIZE list backed by an array
// Immutable factory methods (Java 9+): truly unmodifiable, reject nulls:
List<String> ro = List.of("a", "b");
Map<String, Integer> m = Map.of("a", 1, "b", 2);
Arrays.asList(...) is fixed-size: you can set an
element but add/remove throws. And
Collections.unmodifiableList returns a view — if you keep a reference to
the original backing list and change that, the "unmodifiable" view reflects it. For
genuinely immutable copies, use List.of(...) / List.copyOf(...).
09. Complexity cheat sheet
| Structure | Access | Search | Insert | Delete | Ordered? |
|---|---|---|---|---|---|
ArrayList |
O(1) | O(n) | O(1)* / O(n) mid | O(n) | insertion |
LinkedList |
O(n) | O(n) | O(1) at ends | O(1) at node | insertion |
HashMap/HashSet |
— | O(1) avg | O(1) avg | O(1) avg | none |
LinkedHashMap/Set |
— | O(1) avg | O(1) avg | O(1) avg | insertion |
TreeMap/TreeSet |
— | O(log n) | O(log n) | O(log n) | sorted |
ArrayDeque |
— | O(n) | O(1) at ends | O(1) at ends | insertion |
PriorityQueue |
O(1) peek | O(n) | O(log n) | O(log n) min | by priority |
* amortised — occasional resize copies are O(n) but rare.
10. How to choose — a decision guide
✓ Pick by the question you're answering
- "Ordered list, index access, duplicates OK" → ArrayList
- "Unique items, order doesn't matter" → HashSet
- "Unique items, keep insertion order" → LinkedHashSet
- "Unique items, kept sorted / range queries" → TreeSet
- "Look up a value by a key" → HashMap
- "…and keep keys sorted" → TreeMap
- "Queue / stack / both ends" → ArrayDeque
- "Always pull the smallest / highest-priority" → PriorityQueue
✗ Don't
- Reach for
LinkedListwithout a measured reason. - Use legacy
Vector/Stack/Hashtablein new code. -
Use a
HashMapfrom multiple threads — useConcurrentHashMap.
11. Gotchas — where Java surprises you
1. Autoboxing in collections is silent and costly.
List<Integer> stores boxed Integer objects, not
ints. In a tight numeric loop that's a lot of allocation. And
list.remove(2) removes index 2, while
list.remove(Integer.valueOf(2)) removes the value 2 — a classic bug.
2. HashMap iteration order is not guaranteed — and can change between runs.
Never rely on it. Use LinkedHashMap if you need a predictable order.
3. Storing objects with broken equals/hashCode in a Set/Map.
Duplicates sneak in, or lookups fail. Always override both together for anything used as a key or set element. Records (Java 16+) generate correct versions for you.
4. TreeSet/TreeMap use compareTo, not
equals, to detect duplicates.
If your comparator says two different objects are "equal" (returns 0), the second is dropped —
even if equals says they differ.
12. Interview Q&A
Q: How does HashMap work internally?
An array of buckets. put hashes the key, spreads the bits, maps it to a bucket with
hash & (n-1), and stores it; collisions chain in the bucket (list, or a
red-black tree once 8+ entries). At 75% full it doubles and rehashes. Lookups are O(1) average,
O(log n) worst case since Java 8.
Q: ArrayList vs LinkedList?
ArrayList = growable array, O(1) random access, O(n) middle inserts, cache-friendly. LinkedList = node chain, O(1) end operations, O(n) access, more memory. Default to ArrayList; LinkedList mainly earns its place as a deque.
Q: Why must a good HashMap key be immutable?
Because its bucket is chosen from its hashCode at insertion. Mutate a field that changes the hashCode and the entry becomes unreachable — filed under the old bucket, searched under the new one.
Q: HashMap vs Hashtable vs ConcurrentHashMap?
HashMap: fast, not thread-safe, allows one null key. Hashtable: legacy, fully synchronized, no nulls. ConcurrentHashMap: thread-safe with fine-grained locking/CAS (locks per bucket, not the whole map), no nulls — the modern choice for concurrency.
13. Cheat sheet
-
Two trees:
Collection(List/Set/Queue) andMap(separate). -
Default picks:
ArrayList,HashSet,HashMap,ArrayDeque. -
Need order?
LinkedHashSet/LinkedHashMap(insertion) orTreeSet/TreeMap(sorted). - HashMap: 16 buckets, load factor 0.75, doubles on resize, treeifies buckets at 8.
-
Keys & set elements must have correct, immutable
equals/hashCode. -
Mutating while iterating? Use
Iterator.removeorremoveIf, never the collection directly. -
Sorting:
Comparable= one natural order in the class;Comparator= many orders from outside. -
Thread-safe map:
ConcurrentHashMap, notHashtable.