Lesson 15 of 71
Lists, Deques, and Iteration
This lesson covers the list and deque implementations in java.util, how they store elements, and how iteration detects modification. After it, a reader can choose between ArrayList, LinkedList, and ArrayDeque from their memory layout and explain the exceptions they throw.
| Type | Storage | Typical use |
|---|---|---|
ArrayList |
One Object[], grows by about 1.5x |
Default List |
LinkedList |
Doubly linked nodes | Rarely the right choice |
ArrayDeque |
Circular Object[] |
Stack and FIFO queue |
List.of |
Immutable, compact classes | Constants, return values |
Arrays.asList |
Fixed-size view over an array | Wrapping an existing array |
The interfaces form this hierarchy (Java 21 added the Sequenced* interfaces):
Iterable
└── Collection
├── SequencedCollection (Java 21)
│ ├── List
│ ├── Deque (extends Queue)
│ └── SequencedSet ── SortedSet ── NavigableSet
├── Set
└── Queue
Map ── SequencedMap (Java 21) ── SortedMap ── NavigableMapExample 1: ArrayList growth
An ArrayList keeps its elements in an Object[] field named elementData and a size. Capacity is the array length; size is the number of elements in use. The JDK 25 source of ArrayList.grow creates a 10-slot array on the first add to a list made with new ArrayList<>(), then grows by oldCapacity >> 1 (half the old capacity) each time the array is full. The example reads the private field by reflection, which needs --add-opens java.base/java.util=ALL-UNNAMED as explained in Packages, Modules, and Reflection.
import java.lang.reflect.Field;
import java.util.ArrayList;
public class Growth {
static int capacity(ArrayList<?> list) throws Exception {
Field f = ArrayList.class.getDeclaredField("elementData");
f.setAccessible(true);
return ((Object[]) f.get(list)).length;
}
public static void main(String[] args) throws Exception {
ArrayList<Integer> list = new ArrayList<>();
System.out.println("empty: capacity " + capacity(list));
int last = -1;
for (int i = 0; i < 100; i++) {
list.add(i);
int cap = capacity(list);
if (cap != last) {
System.out.println("size " + list.size() + " -> capacity " + cap);
last = cap;
}
}
list.trimToSize();
System.out.println("after trimToSize: " + capacity(list));
list.ensureCapacity(500);
System.out.println("after ensureCapacity(500): " + capacity(list));
}
}empty: capacity 0
size 1 -> capacity 10
size 11 -> capacity 15
size 16 -> capacity 22
size 23 -> capacity 33
size 34 -> capacity 49
size 50 -> capacity 73
size 74 -> capacity 109
after trimToSize: 100
after ensureCapacity(500): 500The sequence 10, 15, 22, 33, 49, 73, 109 is n + n/2 each time. Each grow copies the array with Arrays.copyOf, so one add can cost O(n), but the total copy work over n adds is O(n): amortized O(1) per add. ensureCapacity grows once before a bulk insert of known size; trimToSize drops unused slots. new ArrayList<>(n) sets the initial capacity directly.
Example 2: LinkedList costs
A LinkedList stores each element in a separate node object with item, next, and prev fields. get(i) walks from the nearer end, so it is O(n). Inserting at a known node is O(1), but reaching the node is O(n) unless an iterator is already there. Each node is an extra object, and the nodes are scattered in memory, which costs cache misses (see CPU Caches and Locality).
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
public class LinkedCosts {
public static void main(String[] args) {
List<Integer> array = new ArrayList<>();
LinkedList<Integer> linked = new LinkedList<>();
for (int i = 0; i < 10; i++) { array.add(i); linked.add(i); }
// Index-based loop on LinkedList: each get walks nodes.
long steps = 0;
for (int i = 0; i < linked.size(); i++) steps += Math.min(i, linked.size() - 1 - i) + 1;
System.out.println("node visits for indexed loop over 10: " + steps);
// Insert after every even element using a ListIterator: O(1) per insert.
ListIterator<Integer> it = linked.listIterator();
while (it.hasNext()) {
if (it.next() % 2 == 0) it.add(-1);
}
System.out.println(linked);
System.out.println("first=" + linked.getFirst() + " last=" + linked.getLast());
}
}node visits for indexed loop over 10: 30
[0, -1, 1, 2, -1, 3, 4, -1, 5, 6, -1, 7, 8, -1, 9]
first=0 last=9The step count models how LinkedList.node(i) walks from whichever end is closer. For n elements an indexed loop visits about n²/4 nodes. In practice ArrayList (or ArrayDeque for both ends) is faster for almost all workloads, including many middle inserts, because System.arraycopy moves contiguous memory quickly. LinkedList fits when a ListIterator stays positioned while many inserts and removes happen at that point.
Example 3: ArrayDeque as stack and queue
ArrayDeque stores elements in a circular array: head is the index of the first element, tail the index after the last, and both wrap around modulo the array length. Adding or removing at either end is O(1) and does not shift elements. It rejects null, because poll uses null to mean "empty". It replaces both Stack (synchronized, extends Vector) and LinkedList as a queue.
import java.util.ArrayDeque;
import java.util.Deque;
public class DequeUse {
public static void main(String[] args) {
Deque<String> stack = new ArrayDeque<>();
stack.push("a"); stack.push("b"); stack.push("c"); // addFirst
System.out.println("stack pop: " + stack.pop() + stack.pop() + stack.pop());
Deque<String> queue = new ArrayDeque<>();
queue.offer("1"); queue.offer("2"); queue.offer("3"); // addLast
System.out.println("queue poll: " + queue.poll() + queue.poll() + queue.poll());
System.out.println("poll on empty: " + queue.poll());
try {
queue.remove();
} catch (java.util.NoSuchElementException e) {
System.out.println("remove on empty: NoSuchElementException");
}
try {
queue.add(null);
} catch (NullPointerException e) {
System.out.println("null rejected");
}
// Balanced brackets with the deque as a stack.
String s = "{[()()]}";
Deque<Character> open = new ArrayDeque<>();
boolean ok = true;
for (char c : s.toCharArray()) {
switch (c) {
case '(', '[', '{' -> open.push(c);
default -> {
char expected = c == ')' ? '(' : c == ']' ? '[' : '{';
if (open.isEmpty() || open.pop() != expected) ok = false;
}
}
}
System.out.println(s + " balanced: " + (ok && open.isEmpty()));
}
}stack pop: cba
queue poll: 123
poll on empty: null
remove on empty: NoSuchElementException
null rejected
{[()()]} balanced: trueDeque has two method families: offer/poll/peek return false or null on failure, and add/remove/element throw. In the JDK 25 source a default ArrayDeque starts with a 17-slot array (16 elements plus one empty slot that separates head and tail), and grows by oldCapacity + 2 while below 64 and by half above that.
Example 4: iterators and fail-fast behavior
An Iterator walks a collection with hasNext and next; the enhanced for loop compiles to one. Collections in java.util keep a counter, modCount, that every structural change (add, remove, clear) increments. The iterator records the value at creation and throws ConcurrentModificationException when it changes. This is fail-fast behavior: it detects the bug on a best-effort basis and is not a thread-safety mechanism.
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
public class FailFast {
public static void main(String[] args) {
List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
try {
for (Integer n : nums) {
if (n % 2 == 0) nums.remove(n); // structural change during iteration
}
} catch (ConcurrentModificationException e) {
System.out.println("ConcurrentModificationException, list now " + nums);
}
nums = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
Iterator<Integer> it = nums.iterator();
while (it.hasNext()) {
if (it.next() % 2 == 0) it.remove(); // the iterator updates its expected count
}
System.out.println("iterator.remove: " + nums);
List<Integer> quiet = new ArrayList<>(List.of(1, 2, 3));
for (Integer n : quiet) {
if (n == 2) quiet.remove(n); // removes the second-to-last element
}
System.out.println("no exception, list now " + quiet);
}
}ConcurrentModificationException, list now [1, 3, 4, 5, 6]
iterator.remove: [1, 3, 5]
no exception, list now [1, 3]The last loop shows why fail-fast is best effort. After removing 2, size becomes 2 and the cursor is 2, so hasNext returns false and the check in next never runs. Code must not rely on the exception.
Example 5: removeIf and bulk operations
Collection.removeIf (Java 8) removes all elements matching a condition in one pass. ArrayList overrides it to mark matches first and then compact the array once, which is O(n) instead of O(n²) for repeated remove(i). The condition is passed as a lambda, n -> n % 3 == 0; lambdas are taught in Lambdas and Functional Interfaces.
import java.util.ArrayList;
import java.util.List;
public class RemoveIf {
public static void main(String[] args) {
List<Integer> nums = new ArrayList<>();
for (int i = 1; i <= 12; i++) nums.add(i);
boolean changed = nums.removeIf(n -> n % 3 == 0);
System.out.println(changed + " " + nums);
List<Integer> other = new ArrayList<>(List.of(1, 2, 4, 99));
other.retainAll(nums); // keep only elements also in nums
System.out.println(other);
nums.subList(0, 4).clear(); // range removal through a view
System.out.println(nums);
// The Integer vs int overload trap.
List<Integer> xs = new ArrayList<>(List.of(10, 20, 30));
xs.remove(1); // remove(int index)
xs.remove(Integer.valueOf(10)); // remove(Object)
System.out.println(xs);
}
}true [1, 2, 4, 5, 7, 8, 10, 11]
[1, 2, 4]
[7, 8, 10, 11]
[30]subList returns a view backed by the list; clearing it removes the range with one array shift. List<Integer>.remove(1) picks remove(int index), not remove(Object), because overload resolution prefers a match without boxing.
Example 6: List.of, Arrays.asList, and unmodifiable views
List.of (Java 9) returns an immutable list: every mutator throws UnsupportedOperationException, and null elements are rejected at creation and in contains. Arrays.asList returns a fixed-size list backed by the array: set works and writes through to the array, but add and remove throw. Collections.unmodifiableList returns a read-only view that still shows changes to the underlying list; List.copyOf makes an independent immutable copy.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class ImmutableLists {
static void attempt(String label, Runnable r) {
try { r.run(); System.out.println(label + ": ok"); }
catch (RuntimeException e) { System.out.println(label + ": " + e.getClass().getSimpleName()); }
}
public static void main(String[] args) {
List<String> fixed = List.of("a", "b");
attempt("List.of add", () -> fixed.add("c"));
attempt("List.of with null", () -> List.of("a", null));
attempt("List.of contains(null)", () -> fixed.contains(null));
String[] array = {"x", "y"};
List<String> view = Arrays.asList(array);
view.set(0, "X");
System.out.println("array[0] = " + array[0]);
attempt("asList add", () -> view.add("z"));
int[] prims = {1, 2, 3};
System.out.println("Arrays.asList(int[]).size() = " + Arrays.asList(prims).size());
List<String> source = new ArrayList<>(List.of("p"));
List<String> readOnly = Collections.unmodifiableList(source);
List<String> copy = List.copyOf(source);
source.add("q");
System.out.println("view " + readOnly + ", copy " + copy);
}
}List.of add: UnsupportedOperationException
List.of with null: NullPointerException
List.of contains(null): NullPointerException
array[0] = X
asList add: UnsupportedOperationException
Arrays.asList(int[]).size() = 1
view [p, q], copy [p]Arrays.asList(int[]) returns a List<int[]> with one element, because a type argument cannot be primitive and the whole array becomes the single varargs element. The helper takes a Runnable lambda only to keep the try/catch in one place.
Example 7: sequenced collections (Java 21)
SequencedCollection (Java 21, JEP 431) gives every collection with a defined encounter order the same methods: getFirst, getLast, addFirst, addLast, removeFirst, removeLast, and reversed. Before Java 21, the last element of a List was list.get(list.size() - 1) and of a LinkedHashSet required iterating. reversed() returns a view, not a copy.
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.SequencedCollection;
public class Sequenced {
static String ends(SequencedCollection<String> c) {
return c.getFirst() + ".." + c.getLast();
}
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("b", "c"));
list.addFirst("a");
list.addLast("d");
System.out.println(list + " " + ends(list));
List<String> rev = list.reversed();
list.add("e");
System.out.println("reversed view: " + rev);
LinkedHashSet<String> set = new LinkedHashSet<>(List.of("x", "y", "z"));
set.addFirst("z"); // moves z to the front
System.out.println(set + " " + ends(set));
ArrayDeque<String> dq = new ArrayDeque<>(List.of("1", "2"));
System.out.println(ends(dq) + " removed " + dq.removeLast());
try {
List.of("k").addFirst("j");
} catch (UnsupportedOperationException e) {
System.out.println("immutable list: UnsupportedOperationException");
}
}
}[a, b, c, d] a..d
reversed view: [e, d, c, b, a]
[z, x, y] z..y
1..2 removed 2
immutable list: UnsupportedOperationExceptionOne method signature, ends(SequencedCollection<String>), accepts a list, a linked set, and a deque. LinkedHashSet.addFirst moves an existing element instead of adding a duplicate. On an empty collection, getFirst throws NoSuchElementException.
Interview points
ArrayListgrows to 10 on first add, then by half its capacity; adds are amortized O(1),getis O(1), middle insert is O(n) array shift.LinkedList.get(i)is O(n); each element is a separate node object.ArrayDequeis the usual replacement for stacks and queues.ArrayDequeis a circular array that rejectsnull;Stackis a legacy synchronized class.- Fail-fast iterators compare
modCountand throwConcurrentModificationException; it is best effort, single-thread misuse detection, not a concurrency guarantee. - Remove during iteration with
Iterator.removeorremoveIf, never withlist.removeinside a for-each loop. List.ofis immutable and null-hostile;Arrays.asListis fixed-size and writes through;unmodifiableListis a view;List.copyOfis a copy.list.remove(1)on aList<Integer>removes by index.- Java 21 sequenced collections add
getFirst,getLast,addFirst,reversedto lists, deques, linked sets, and sorted sets.
Summary
Collectionsplits intoList,Set, andQueue/Deque; Java 21 insertedSequencedCollectionabove ordered ones.ensureCapacityand the capacity constructor avoid repeated grows;trimToSizereleases slack.ArrayDequegrows bycapacity + 2below 64 and by half above.- Structural modification increments
modCount; iterators check it innext. - Immutable lists throw
UnsupportedOperationExceptionon every mutator, includingaddFirst.
