Learning goals
By the end of this chapter you should be able to:
- Choose between
ArrayListandLinkedListfor a workload — not by habit, by access pattern - Use core
Listoperations: indexed access, iterator edits, sublist views, bulk ops - Apply
List.of/List.copyOffor immutable snapshots and know whenArrayListmust grow - Recognize fail-fast iteration and
SequencedCollectionend APIs (Java 21+)
From arrays to growable lists
You already know arrays: fixed length, int[], fast index access, no built-in insert/remove in the middle.
A List is the growable, interface-driven evolution:
List<String> names = new ArrayList<>();
names.add("Ana");
names.add("Bo");
names.set(0, "Alice");
names.remove(1);
Same indexed mental model as an array, but size() changes and the compiler enforces generics (List<String> not raw List).
When to reach for a list instead of an array:
- Size unknown at compile time or changes at runtime
- You need
ListAPI integration (streams,sort,subList, collections utilities) - You want to program to an interface and swap implementations
Default choice for a mutable List: ArrayList. Reach for LinkedList only when iterator-heavy middle inserts or Deque dual use justify the extra per-node cost — details below.
A List is an ordered sequence: each element has an index from 0 to size()-1. ArrayList and LinkedList are not safe for concurrent writes without external synchronization—use Collections.synchronizedList, a java.util.concurrent queue/map design, or explicit locks when threads share a mutable list.
Under each implementation: Why / vs alternatives explains what problem it solves compared to the others.
ArrayList — not concurrent
Stores elements in a resizable array. When the array is full, a larger array is allocated and elements are copied over. Index access is direct: get(i) and set(i, v) are very fast. Inserting in the middle may require shifting many elements, so it is O(n) in the worst case. Appending at the end is usually amortized O(1) (occasional copy when growing).
Initialize: List<T> list = new ArrayList<>(); — optional capacity: new ArrayList<>(expectedSize).
Sharing across threads: wrap for mutual exclusion on every method (single lock—simple but can bottleneck):
List<String> list = Collections.synchronizedList(new ArrayList<>());
Heavy concurrent writes → often CopyOnWriteArrayList or java.util.concurrent queues instead; compare ConcurrentHashMap vs Collections.synchronizedMap in Maps.
Why / vs alternatives: Default mutable list: fast random index and cache-friendly contiguous storage. LinkedList wins when you mostly insert/remove at a ListIterator cursor or need List + Deque in one type—not for beating ArrayList on get(i) random access.
add(e)— appends at the end; may grow the array.add(i, e)— inserts at indexi, shifts following elements right.get(i)— returns the element at indexi.set(i, e)— replaces the element ati, returns the old value.remove(i)— removes the element ati, shifts following elements left.remove(Object)— removes the first occurrence equal to the argument (usesequals).size()— number of elements.isEmpty()— whethersize()is zero.clear()— removes all elements.contains(o)— whether some elementequalsthe argument.indexOf(o)/lastIndexOf(o)— first or last index of a match, or-1.iterator()— forward iteration; fail-fast if the list is structurally modified while iterating (except through the iterator’s ownremove).listIterator()/listIterator(i)— bidirectional iterator; can insert and remove at cursor.subList(from, to)— view of the range[from, to); changes show up in the original list and vice versa.toArray()— copies elements into an array.
Java 21+: List extends SequencedCollection. End operations have standard names: getFirst() / getLast() (instead of get(0) / get(size() - 1)), addFirst() / addLast(), removeFirst() / removeLast() — same spelling Deque uses for two-ended access on ArrayList and LinkedList.
LinkedList — not concurrent
A doubly linked list: each node has prev and next pointers. No random access from index in O(1)—get(i) walks from an end, O(n). Insert/remove at the iterator cursor is O(1) per step. Also implements Deque (queue/stack at both ends).
Initialize: List<T> list = new LinkedList<>(); — or new LinkedList<>(anotherCollection).
Sharing across threads: pick one synchronized wrapper for one backing list (do not stack wrappers):
List<String> list = Collections.synchronizedList(new LinkedList<>());
// or: Deque<String> dq = Collections.synchronizedDeque(new LinkedList<>());
Why / vs alternatives: Solves O(1) structural edits at a moving iterator and two-ended queue/stack while still being a List. ArrayList shifts elements on middle insert; LinkedList avoids that when you already have the iterator. For queue/stack only on one thread, ArrayDeque is usually faster (no per-element nodes).
Implements the same List methods as ArrayList (with different performance). Especially useful:
listIterator()— walk the list andadd/removein O(1) per step at the iterator position.addFirst/addLast/removeFirst/removeLast— Deque API (also onLinkedList).
List operations (all implementations) — varies
These are defined on the List interface; behavior is the same name everywhere, cost differs by implementation.
Initialize (immutable): List<T> list = List.of(a, b, c); — or List.copyOf(collection).
Why / vs alternatives: Fixed content published to callers without giving them a mutable ArrayList. List.of for small literal lists; List.copyOf from any collection. No add/remove—use ArrayList when the collection must grow.
addAll(c)— appends every element of collectionc.removeAll(c)/retainAll(c)— remove elements that are / are not inc.removeIf(predicate)— removes each element matching the predicate (Java 8+).replaceAll(operator)— replaces each element withoperator.apply(element).sort(comparator)— stable sort in place.forEach(action)— runsactionon each element in order.stream()/parallelStream()— stream pipeline from this list.
Factories: List.of(a, b, ...) builds a fixed-size, immutable list. List.copyOf(collection) copies into an immutable list.
Next: Sets
Practice checkpoint
-
You need fast random access by index and mostly append at the end —
ArrayListorLinkedList? Answer:ArrayList— O(1) indexed get; append amortized O(1). -
What happens if you modify a list structurally while iterating with enhanced for-loop (not via iterator)? Answer:
ConcurrentModificationException(fail-fast) on the next iteration step. -
List.of("a", "b").add("c")— compile or runtime issue? Answer: runtime —List.ofreturns immutable list;addthrowsUnsupportedOperationException. -
When is
subList(from, to)dangerous? Answer: it is a view backed by the original; structural changes to either can invalidate the other or throw unexpectedly.
Interview angles
- ArrayList growth: new larger array + copy when capacity exceeded; pre-size with constructor when you know N.
- LinkedList vs ArrayList: LinkedList wins at iterator cursor inserts; loses on
get(i)random access. - SynchronizedList vs CopyOnWriteArrayList: former locks whole list; latter copies array on write — read-heavy workloads.
- SequencedCollection (21+):
getFirst/getLastunify end access acrossList,Deque, ordered sets/maps.