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.

The mental model to hold

Two separate family trees. Collection covers things that hold single elementsList, 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.

The two family trees
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 by equals).
  • 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.
"Legacy" collections

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.

Lists

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.

ArrayList basics
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)
The honest interview answer

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.

Sizing tip

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.

Sets

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)
Same data, different order
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
Which Set to reach for

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.

Maps — the big one

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 basics
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

  1. Call key.hashCode() to get a raw 32-bit number.
  2. Spread the bits (Java does h ^ (h >>> 16)) so that even keys with similar hashes land in different buckets. This mixing reduces collisions.
  3. Map that to a bucket index with hash & (n - 1), where n is the table length. (This works as a fast modulo because the table length is always a power of two — that's why it is.)
  4. 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 hashCode then equals. If a key equals an existing one, overwrite its value; otherwise append the new entry.
Why keys need 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
Never use a mutable key

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 — a HashMap that 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 like firstKey, floorKey, headMap, subMap.
  • Hashtable — legacy, fully synchronized, no null keys/values. For thread-safety today use ConcurrentHashMap instead.
Nulls

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.

Queues & Deques

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

ArrayDeque as both a queue and a stack
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
Use 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
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());
Working with collections

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.

Ways to iterate
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+)
ConcurrentModificationException

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 via compareTo. Example: String sorts alphabetically, Integer numerically.
  • Comparator<T> — a separate object defining an ordering. You can make many (by name, by age, reversed) without touching the class.
Both, side by side
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
The contract

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.

Handy static helpers
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);
Two "immutable-ish" traps

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

Reference

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 LinkedList without a measured reason.
  • Use legacy Vector/Stack/Hashtable in new code.
  • Use a HashMap from multiple threads — use ConcurrentHashMap.

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) and Map (separate).
  • Default picks: ArrayList, HashSet, HashMap, ArrayDeque.
  • Need order? LinkedHashSet/LinkedHashMap (insertion) or TreeSet/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.remove or removeIf, never the collection directly.
  • Sorting: Comparable = one natural order in the class; Comparator = many orders from outside.
  • Thread-safe map: ConcurrentHashMap, not Hashtable.
Last reviewed · July 2026 · part of knowledge-base