PrepZone Logo
PrepZone

ArrayList and LinkedList

How each one stores elements, what that costs per operation, and which to default to.

Why this matters

  • The textbook answer — "LinkedList for insertions, ArrayList for reads" — is misleading, and knowing why marks the difference between memorised and understood.
  • ArrayList growth is the mechanism behind a surprising amount of garbage in list-heavy code, and one constructor argument removes it.
  • These two are the most frequently compared classes in Java interviews.

How each one stores elements

ArrayList — one array
[0] A
[1] B
[2] C

get(i) is instant. Inserting in the middle shifts everything after it.

LinkedList — linked nodes
Node Aprev · next →
Node Bprev · next →
Node Cprev · next → null

get(i) walks from an end. Insert is pointer surgery.

ArrayList stores elements next to each other, so index access is one calculation. LinkedList must walk node by node, but inserting in the middle only relinks two pointers.
  • ArrayList wraps an Object[]. Element i sits at offset i, so indexed access is a single calculation. Inserting in the middle shifts everything after it.
  • LinkedList is a doubly linked list of nodes. Each node holds the element plus a reference forward and backward. Reaching index i means walking i links.

Cost, honestly

AspectOperationArrayList vs LinkedList
get(i) / set(i)O(1)O(n) — must walk to the position
add at endAmortised O(1)O(1)
add or remove at index 0O(n) — shifts everythingO(1)
add or remove in the middleO(n) shiftO(n) to find, then O(1) to relink
contains / indexOfO(n)O(n)
Memory per elementOne slot, plus spare capacityA node object with two references
Cache behaviourExcellent — contiguousPoor — nodes scattered on the heap
  • get(i) / set(i)

    OperationO(1)
    ArrayList vs LinkedListO(n) — must walk to the position
  • add at end

    OperationAmortised O(1)
    ArrayList vs LinkedListO(1)
  • add or remove at index 0

    OperationO(n) — shifts everything
    ArrayList vs LinkedListO(1)
  • add or remove in the middle

    OperationO(n) shift
    ArrayList vs LinkedListO(n) to find, then O(1) to relink
  • contains / indexOf

    OperationO(n)
    ArrayList vs LinkedListO(n)
  • Memory per element

    OperationOne slot, plus spare capacity
    ArrayList vs LinkedListA node object with two references
  • Cache behaviour

    OperationExcellent — contiguous
    ArrayList vs LinkedListPoor — nodes scattered on the heap

LinkedList's middle insertion is only O(1) once you are already there, which an index-based call never is.

How ArrayList grows

The internal array has a capacity that is usually larger than the size. When it fills, a new larger array is allocated and the contents copied.

Java
List<Integer> numbers = new ArrayList<>();      // default capacity 10, allocated lazily
for (int i = 0; i < 1_000_000; i++) {
    numbers.add(i);                              // ~30 reallocations and copies along the way
}

// Pre-size it when the count is known or estimable
List<Integer> sized = new ArrayList<>(1_000_000);   // one allocation, no copying

Growth is roughly 1.5× the current capacity. That makes the amortised cost of add constant, because the occasional expensive copy is spread across many cheap appends. But each copy allocates a full array and leaves the old one for the garbage collector.

Practical consequences

  • Pre-size when you can. new ArrayList<>(expected) eliminates every intermediate array.
  • size() is not capacity. A list of 3 elements may hold an array of 10.
  • remove never shrinks the array. Call trimToSize() on the concrete ArrayList if a large list has shrunk permanently.
  • clear() nulls the slots but keeps the capacity, which is efficient for reuse.

Removing while iterating

The enhanced for loop cannot remove safely. There are three correct approaches.

Java
List<String> names = new ArrayList<>(List.of("ana", "bob", "carl", "dee"));

// Wrong: ConcurrentModificationException
// for (String name : names) {
//     if (name.startsWith("b")) names.remove(name);
// }

// 1. removeIf — clearest, and the right default
names.removeIf(name -> name.startsWith("b"));

// 2. An explicit iterator, when the condition needs more than a predicate
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
    if (iterator.next().length() > 3) {
        iterator.remove();           // the only safe removal during iteration
    }
}

// 3. A backwards index loop, when you need the index itself
for (int i = names.size() - 1; i >= 0; i--) {
    if (names.get(i).isEmpty()) names.remove(i);
}

remove has two overloads

Java
List<Integer> numbers = new ArrayList<>(List.of(10, 20, 30));

numbers.remove(1);                      // remove(int index)    → removes 20
numbers.remove(Integer.valueOf(10));    // remove(Object value) → removes the value 10

Both compile. The first takes an index, the second a value, and for a List<Integer> the two are indistinguishable at a glance. Always write Integer.valueOf(...) or cast to Object when you mean the value.

CopyOnWriteArrayList

The thread-safe list, suited to a specific shape of workload.

Java
List<Listener> listeners = new CopyOnWriteArrayList<>();

listeners.add(listener);         // copies the whole array — expensive

for (Listener listener : listeners) {
    listener.onEvent(event);      // iterates a snapshot; no lock, no CME
}

Every mutation copies the entire backing array. Reads and iteration are lock-free and never throw ConcurrentModificationException, because the iterator holds the snapshot it started with.

When it fits

  • Many reads, very few writes — event listener registries are the canonical example.
  • Iteration must never throw, and seeing a slightly stale view is acceptable.
  • Not for write-heavy use. Copying the array per write is quadratic in a loop.

Other list operations worth knowing

Java
List<String> names = new ArrayList<>(List.of("carl", "ana", "bob"));

Collections.sort(names);                        // natural order, in place
names.sort(Comparator.comparing(String::length));   // the modern equivalent
Collections.reverse(names);
Collections.shuffle(names);

List<String> middle = names.subList(1, 3);      // a VIEW, not a copy
int position = Collections.binarySearch(names, "bob");   // sorted list required

String[] array = names.toArray(new String[0]);  // the idiomatic conversion
List<String> fromArray = new ArrayList<>(Arrays.asList(array));

Common misreadings

  • "LinkedList is faster for insertion." Only when you are already at the position. Index-based insertion pays O(n) to get there.
  • "size() tells you how much memory the list uses." Capacity is usually larger and only grows.
  • "remove shrinks the backing array." It does not. Use trimToSize() if that matters.
  • "ArrayList is thread-safe if you only read." Safe only if no thread writes at all. Any concurrent write needs CopyOnWriteArrayList or external synchronisation.
  • "subList returns a copy." It is a live view with a two-way relationship to the original.
  • "toArray() with no argument gives a typed array." It returns Object[]. Pass new String[0] for a typed one.

Quick recall

Everything you need if you only revisit this box.

  • ArrayList is a resizable array: O(1) indexed access, O(n) insertion other than at the end, excellent cache behaviour.
  • LinkedList is a doubly linked list: O(1) relinking but O(n) to reach a position, plus a node object per element.
  • Default to ArrayList. Use ArrayDeque for queue or stack behaviour, not LinkedList.
  • Growth is ~1.5× with a full array copy. Pre-size with new ArrayList<>(expected) when the count is known.
  • Remove during iteration with removeIf, an explicit Iterator.remove(), or a backwards index loop.
  • remove(int) removes by index and remove(Object) by value — a real hazard for List<Integer>.
  • CopyOnWriteArrayList suits read-heavy, write-rare use; every write copies the array.
  • subList is a view, not a copy.

Test yourself

Answer these before moving on — recall is what makes it stick.