Collections in Depth
Iterators, immutable collections, deques and queues, ConcurrentModificationException, PriorityQueue, NavigableSet, sequenced collections, and LinkedHashMap.
8 questions
MiddleTheoryVery commonHow do HashMap, LinkedHashMap and TreeMap differ in order and cost?
How do HashMap, LinkedHashMap and TreeMap differ in order and cost?
HashMap is a hash table: O(1) average get/put and no ordering guarantee whatsoever. LinkedHashMap extends it with a doubly-linked list threaded through the entries, preserving insertion order — or access order, which turns it into an LRU cache via removeEldestEntry — still O(1), at a small memory cost. TreeMap is a red-black tree: keys sorted by Comparable/Comparator, O(log n) operations, and no null key.
Common mistakes
- ✗Assuming
HashMapiteration order is stable or reflects insertion order - ✗Thinking
TreeMapsorting is free rather than costing O(log n) per operation - ✗Forgetting that access-order mode is what makes
LinkedHashMapusable as an LRU cache
Follow-up questions
- →How does
removeEldestEntryturn aLinkedHashMapinto a bounded LRU cache? - →Why does
TreeMapreject anullkey whileHashMapallows exactly one?
JuniorTheoryCommonWhy prefer ArrayDeque over LinkedList as a stack or a queue?
Why prefer ArrayDeque over LinkedList as a stack or a queue?
ArrayDeque implements Deque over a resizable circular array, so additions and removals at both ends are amortized O(1) with contiguous, cache-friendly storage. LinkedList also implements Deque, but every element needs a separate node holding two links, which costs memory, locality and GC pressure. ArrayDeque is the recommended stack (over the legacy Stack) and queue; it just forbids null elements.
Common mistakes
- ✗Reaching for the legacy
Stackclass, which is synchronized and extendsVector - ✗Assuming the O(1) node splice of
LinkedListmakes it the faster queue in practice - ✗Forgetting that
ArrayDequerejects anullelement with aNullPointerException
Follow-up questions
- →Why does
ArrayDequeforbidnullelements whileLinkedListaccepts them? - →How does
ArrayDequegrow its circular array, and what does that growth cost?
JuniorTheoryCommonHow does List.of() differ from Collections.unmodifiableList()?
How does List.of() differ from Collections.unmodifiableList()?
List.of() builds a genuinely immutable list: it copies the arguments into its own storage, rejects null elements, and hands out no reference to any backing collection, so nothing can ever change it. Collections.unmodifiableList(list) returns only a read-only view — its own mutators throw UnsupportedOperationException, but a write through the original list reference is still visible through that view.
Common mistakes
- ✗Believing
Collections.unmodifiableListfreezes the source list rather than wrapping it in a view - ✗Passing a
nullelement intoList.of(), which throws aNullPointerException - ✗Assuming an immutable list is deeply immutable, when the elements it holds can still be mutable
Follow-up questions
- →Why does
List.of()rejectnullelements whileArrays.asList()accepts them? - →What does
List.copyOf(list)guarantee thatunmodifiableList(list)does not?
MiddleTheoryCommonWhat ordering does a PriorityQueue actually guarantee?
What ordering does a PriorityQueue actually guarantee?
Only the head is ordered. PriorityQueue is a binary heap, so peek/poll always return the least element by natural order or by the supplied Comparator, but iterator() and toString() walk the backing array in heap order, which is not sorted. offer/poll cost O(log n), peek is O(1), and elements of equal priority have no defined relative order — the queue is not stable, so it is not FIFO within a priority.
Common mistakes
- ✗Expecting
for (T t : pq)ortoString()to come out in priority order - ✗Assuming elements of equal priority leave the queue in insertion order
- ✗Thinking
pollis O(1) rather than an O(log n) sift-down of the heap
Follow-up questions
- →How would you make a
PriorityQueuestable for elements of equal priority? - →Why is
remove(Object)O(n) on aPriorityQueuewhilepollis only O(log n)?
JuniorTheoryOccasionalWhat can a ListIterator do that a plain Iterator cannot?
What can a ListIterator do that a plain Iterator cannot?
Iterator traverses any Collection forward only, with hasNext, next, and remove. ListIterator, obtainable only from a List, also moves backward via hasPrevious/previous, reports positions with nextIndex/previousIndex, replaces the last returned element with set, and inserts at the cursor with add. Both are fail-fast on structural change made outside the iterator.
Common mistakes
- ✗Thinking
ListIteratoris available from aSetorMap, not only from aList - ✗Assuming
Iteratorcan move backward or callset/addlike aListIterator - ✗Forgetting
setreplaces the element last returned bynext/previous, not an arbitrary one
Follow-up questions
- →When would you use
ListIterator.setinstead ofList.set(index, e)? - →Why does
addon aListIteratorinsert before the elementnextwould return?
MiddleDebuggingOccasionalFix the loop that drops items from a list while iterating it
Fix the loop that drops items from a list while iterating it
The for-each loop runs on the list's own fail-fast Iterator. names.remove(name) bumps the list's modCount behind the iterator's back, so the next next() finds modCount != expectedModCount and throws ConcurrentModificationException — no second thread is involved. Fix it by mutating through the iterator (it.remove() after it.next()), or simply call names.removeIf(n -> n.length() == 3); iterating over a copy also works.
Common mistakes
- ✗Believing
ConcurrentModificationExceptiononly ever happens with multiple threads - ✗Calling
list.remove(...)inside a for-each instead ofIterator.remove() - ✗Assuming the exception is guaranteed — removing the second-to-last element can slip through
Follow-up questions
- →Why does removing the second-to-last element sometimes avoid the exception entirely?
- →How do the iterators of
CopyOnWriteArrayListandConcurrentHashMapavoid this failure?
MiddleTheoryRareWhat do SequencedCollection, SequencedSet and SequencedMap add?
What do SequencedCollection, SequencedSet and SequencedMap add?
Java 21 gives every collection with a defined encounter order a common supertype. SequencedCollection declares getFirst/getLast, addFirst/addLast, removeFirst/removeLast and reversed(); List, Deque and LinkedHashSet implement it, so list.getFirst() replaces list.get(0). SequencedSet narrows reversed() to a set, and SequencedMap adds firstEntry/lastEntry and putFirst/putLast. reversed() is a live view, not a copy.
Common mistakes
- ✗Expecting
HashSetorHashMapto gain an encounter order — having none, they stay out - ✗Treating
reversed()as a copy rather than a live view backed by the original collection - ✗Thinking
Deque.addFirstandListalready shared a common first/last API before Java 21
Follow-up questions
- →Why can
LinkedHashSetimplementSequencedCollectionwhileHashSetcannot? - →What happens to a
reversed()view when you add an element to the collection behind it?