Why this matters
- The framework has dozens of classes, and picking by habit rather than by shape is how an application ends up doing linear scans where a hash lookup would do.
- Interviews reliably ask you to justify a choice, and "I always use ArrayList" is not a justification.
- Knowing the interface you should declare — as opposed to the class you instantiate — is what keeps code swappable.
The shape of the framework
Two roots, not one. Collection covers things holding single elements; Map holds pairs and is
deliberately separate because its operations have a different shape.
The four interfaces that matter
List— ordered by position, duplicates allowed, indexed access. Use when sequence matters.Set— no duplicates, usually no positional access. Use when membership is the question.Queue/Deque— ordered for processing, with access at the ends. Use for pipelines, buffers and stacks.Map— unique keys mapped to values. Use when you look things up by something other than position.
Choosing, in four questions
| Aspect | What you need | Reach for |
|---|---|---|
| An ordered list, mostly read by index | ArrayList | O(1) indexed access, cache-friendly |
| Heavy insertion and removal at the ends | ArrayDeque | Faster than LinkedList in practice |
| Uniqueness, order irrelevant | HashSet | O(1) membership test |
| Uniqueness, insertion order kept | LinkedHashSet | Predictable iteration |
| Uniqueness, sorted | TreeSet | O(log n), plus range queries |
| Key-value lookup | HashMap | O(1) average get and put |
| Key-value, sorted keys | TreeMap | O(log n), navigable |
| Key-value across threads | ConcurrentHashMap | Concurrent reads, segmented writes |
| Highest-priority item first | PriorityQueue | A binary heap, O(log n) insert |
An ordered list, mostly read by index
What you needArrayListReach forO(1) indexed access, cache-friendlyHeavy insertion and removal at the ends
What you needArrayDequeReach forFaster than LinkedList in practiceUniqueness, order irrelevant
What you needHashSetReach forO(1) membership testUniqueness, insertion order kept
What you needLinkedHashSetReach forPredictable iterationUniqueness, sorted
What you needTreeSetReach forO(log n), plus range queriesKey-value lookup
What you needHashMapReach forO(1) average get and putKey-value, sorted keys
What you needTreeMapReach forO(log n), navigableKey-value across threads
What you needConcurrentHashMapReach forConcurrent reads, segmented writesHighest-priority item first
What you needPriorityQueueReach forA binary heap, O(log n) insert
These nine cover the overwhelming majority of real code.
Cost at a glance
The operation that dominates your code is the one to optimise for.
| Aspect | Operation | Cost by implementation |
|---|---|---|
| get by index | ArrayList O(1) | LinkedList O(n) |
| add at end | ArrayList amortised O(1) | LinkedList O(1) |
| add or remove at front | ArrayList O(n) | ArrayDeque O(1) |
| contains | ArrayList O(n) | HashSet O(1), TreeSet O(log n) |
| get by key | HashMap O(1) average | TreeMap O(log n) |
| iterate in sorted order | TreeMap O(n), free | HashMap requires sorting first |
get by index
OperationArrayList O(1)Cost by implementationLinkedList O(n)add at end
OperationArrayList amortised O(1)Cost by implementationLinkedList O(1)add or remove at front
OperationArrayList O(n)Cost by implementationArrayDeque O(1)contains
OperationArrayList O(n)Cost by implementationHashSet O(1), TreeSet O(log n)get by key
OperationHashMap O(1) averageCost by implementationTreeMap O(log n)iterate in sorted order
OperationTreeMap O(n), freeCost by implementationHashMap requires sorting first
ArrayList's amortised O(1) append hides an occasional O(n) array copy when it grows.
Declare the interface, instantiate the class
// Good: callers depend on the capability, and you can change your mind
List<String> names = new ArrayList<>();
Map<String, Integer> counts = new HashMap<>();
Set<String> seen = new LinkedHashSet<>();
// Avoid: the concrete type leaks into every signature that touches it
ArrayList<String> rigid = new ArrayList<>();
Declaring List means switching to a LinkedList, or to an immutable List.of(...), is a one-line
change. Declaring ArrayList spreads that decision across every method signature it reaches.
Immutable and unmodifiable collections
Three things that are easy to confuse.
// Immutable factories — Java 9+. Reject nulls, no duplicates for Set and Map keys.
List<String> fixed = List.of("a", "b", "c");
Set<Integer> ids = Set.of(1, 2, 3);
Map<String, Integer> ages = Map.of("Ana", 30, "Bo", 25);
// A snapshot copy: independent of the source from this moment on
List<String> snapshot = List.copyOf(mutableSource);
// A view: read-only through this reference, but changes to the source show through
List<String> view = Collections.unmodifiableList(mutableSource);
The distinctions
List.of(...)is genuinely immutable, rejectsnullelements, and is memory-efficient for small sizes.List.copyOf(source)takes a snapshot. Later changes to the source are not visible.Collections.unmodifiableList(source)wraps. The holder cannot modify it, but changes made through the original reference are visible.- All three throw
UnsupportedOperationExceptiononadd,removeorset.
Null tolerance differs
This catches people out because the behaviour is inconsistent across implementations.
| Aspect | Implementation | Null handling |
|---|---|---|
| ArrayList, LinkedList | Nulls allowed as elements | Any number of them |
| HashSet, LinkedHashSet | One null element allowed | It is still a set |
| TreeSet, TreeMap | No nulls | Comparison would require calling compareTo on null |
| HashMap | One null key, many null values | Allowed |
| Hashtable, ConcurrentHashMap | No null keys or values | Ambiguity with absent keys |
| List.of, Set.of, Map.of | No nulls anywhere | Throws NullPointerException |
ArrayList, LinkedList
ImplementationNulls allowed as elementsNull handlingAny number of themHashSet, LinkedHashSet
ImplementationOne null element allowedNull handlingIt is still a setTreeSet, TreeMap
ImplementationNo nullsNull handlingComparison would require calling compareTo on nullHashMap
ImplementationOne null key, many null valuesNull handlingAllowedHashtable, ConcurrentHashMap
ImplementationNo null keys or valuesNull handlingAmbiguity with absent keysList.of, Set.of, Map.of
ImplementationNo nulls anywhereNull handlingThrows NullPointerException
ConcurrentHashMap forbids nulls so that a null return unambiguously means 'absent'.
Methods on the interfaces themselves
Java 8 added default methods that remove most explicit loops.
List<String> names = new ArrayList<>(List.of("ana", "bo", "carl"));
names.removeIf(name -> name.length() < 3); // safe removal, no iterator needed
names.replaceAll(String::toUpperCase); // in-place transform
names.forEach(System.out::println);
Map<String, Integer> counts = new HashMap<>();
counts.merge("ana", 1, Integer::sum); // insert 1, or add to the existing value
counts.computeIfAbsent("bo", key -> expensive(key)); // compute only when missing
counts.getOrDefault("carl", 0); // no null check needed
counts.putIfAbsent("dee", 5);
counts.forEach((key, value) -> System.out.println(key + "=" + value));
merge and computeIfAbsent deserve special note: they replace the
check-then-act pattern that is both verbose and wrong under concurrency.
Legacy classes to avoid
Vector— a synchronisedArrayList. Every method locks, even in single-threaded use. UseArrayList, orCopyOnWriteArrayListwhen you genuinely need thread safety.Hashtable— a synchronisedHashMappredating the framework. UseHashMaporConcurrentHashMap.Stack— extendsVector, so it inherits the locking and exposes indexed access a stack should not have. UseArrayDeque.Enumeration— the pre-Iteratortraversal interface, with no removal support.
These remain in the JDK only for backward compatibility. Seeing one in modern code is a signal that the code is old or was copied from something old.
Common misreadings
- "
MapextendsCollection." It does not. The two hierarchies are separate by design. - "
CollectionsandCollectionare the same."Collectionis the interface;Collectionsis a utility class of static helpers. - "
List.of(...)returns anArrayList." It returns a compact immutable implementation that throws on modification. - "
LinkedListis faster for insertions." Only at a position you already hold. Reaching the middle is O(n), andArrayDequebeats it at both ends. - "Every collection accepts null."
TreeSet,TreeMap,ConcurrentHashMapand all theof(...)factories do not. - "
Vectoris the thread-safe list to use." Its per-method locking neither performs well nor makes compound operations safe.
Quick recall
Everything you need if you only revisit this box.
- Pick by shape: duplicates? order? keyed or positional? concurrent?
CollectionandMapare separate roots.List,SetandQueueextendCollection.- Defaults worth memorising:
ArrayList,HashMap,HashSet,ArrayDeque— withLinkedHashXfor order andTreeXfor sorting. - Declare the interface, instantiate the class, so the choice stays changeable.
List.ofis immutable,List.copyOfis a snapshot,Collections.unmodifiableListis a live view.Arrays.asListis a fixed-size array view.- Null tolerance varies:
TreeSet/TreeMap,ConcurrentHashMapand theof(...)factories reject nulls. - Use
removeIf,merge,computeIfAbsentandgetOrDefaultinstead of check-then-act loops. - Avoid
Vector,Hashtable,StackandEnumerationin new code.
Test yourself
Answer these before moving on — recall is what makes it stick.
- What are collection factory methods (List.of, Set.of, Map.of) introduced in Java 9? How do they differ from Arrays.asList?
- Explain the Iterator pattern. How is it implemented in the Java Collections Framework?
- Differentiate between Collections.unmodifiableList(), List.of(), and Collections.singletonList().