ArrayList vs LinkedList in Java: The List API Explained
ArrayList is a growable array with O(1) get; LinkedList is a chain of nodes, rarely faster. How each works, the List API, and the remove and subList traps.
- Course: Java study plan
- Module: Collections framework
- Kind: Lesson
- Reading time: 14 min
- Runtime: OpenJDK 18
What is the difference between ArrayList and LinkedList in Java?
ArrayList and LinkedList differ in how they store elements. ArrayList keeps them in a growable array, so get(i) and appending are O(1), but inserting at the front shifts every element. LinkedList is a doubly linked chain of nodes — O(1) at either end, O(n) to reach an index — with more memory per element and poor cache behaviour. ArrayList is the right choice almost always.
Lesson
A List is an ordered sequence with index access and duplicates allowed — the collection you reach for first. ArrayList implements it on a growable array and is the right choice almost always; LinkedList implements it on linked nodes and is the right choice in a narrow set of cases that interviewers like to ask about. This lesson covers how each works, the full List API with its traps (remove(int) versus remove(Object), subList views, Arrays.asList), and how to use lists efficiently.
ArrayList internals
An ArrayList holds an Object[] and a size. add(x) writes at size++; when the array is full it allocates a new one 1.5× larger and copies (Arrays.copyOf). Amortised over many adds, each costs O(1). get(i) is a bounds check and an array read — O(1). add(0, x) or remove(0) shifts every element — O(n). Capacity is separate from size: new ArrayList<>(10_000) pre-sizes the array when the count is known and avoids the regrowth copies; trimToSize() releases slack.
LinkedList internals
A doubly linked chain of Node{item, prev, next} with head and tail. addFirst/addLast/removeFirst/removeLast are O(1); get(i) walks from the nearer end — O(n); every node is a separate heap object (three references plus the header), so memory and cache behaviour are far worse than an array. Its one genuine advantage — O(1) insert/remove at a position you already hold via an iterator — is rarely needed. For a stack or queue, ArrayDeque beats it; for a list, ArrayList does. Joshua Bloch, who wrote it, has said he never uses it.
The List API
List<String> xs = new ArrayList<>(List.of("a", "b", "c"));
xs.add("d"); // append
xs.add(1, "z"); // insert at index (shifts)
xs.get(0); // "a"
xs.set(0, "A"); // replace, returns the old element
xs.remove(1); // by INDEX: removes "z", returns it
xs.remove("d"); // by OBJECT: removes the first equal element, returns boolean
xs.indexOf("b"); // first index or -1; lastIndexOf too
xs.contains("b"); // linear scan
xs.size(); xs.isEmpty(); xs.clear();
xs.addAll(other); xs.removeAll(other); xs.retainAll(other);
xs.subList(1, 3); // a VIEW of [1, 3)
xs.sort(null); // natural order (Comparable); or a Comparator
xs.replaceAll(String::toUpperCase);
xs.removeIf(s -> s.isEmpty());
xs.toArray(new String[0]); // or xs.toArray(String[]::new)
Collections.reverse(xs); Collections.shuffle(xs); Collections.swap(xs, 0, 1);
The remove trap
List<Integer> nums = new ArrayList<>(List.of(10, 20, 30));
nums.remove(1); // remove(int index): removes 20
nums.remove(Integer.valueOf(10)); // remove(Object): removes the element 10
nums.remove((Integer) 30); // same, by cast
With List<Integer>, an int argument selects remove(int index) by exact match (Module 5) — passing the value 1 removes the element at index 1. Box explicitly to remove by value.
subList is a view
List<Integer> big = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> mid = big.subList(1, 4); // [2, 3, 4], backed by big
mid.set(0, 99); // big is now [1, 99, 3, 4, 5]
mid.clear(); // big is now [1, 5] — the idiom for removing a range
big.add(6); // structural change to big…
mid.size(); // …invalidates mid: ConcurrentModificationException
subList shares storage with the parent. It is the efficient way to clear or operate on a range, and a trap if you keep it around while modifying the parent. Copy with new ArrayList<>(big.subList(1, 4)) when you want independence.
Building lists
List<String> a = new ArrayList<>(); // growable, empty
List<String> b = new ArrayList<>(existing); // a growable COPY
List<String> c = List.of("x", "y"); // immutable, no nulls (Java 9)
List<String> d = List.copyOf(existing); // immutable copy (returns the same instance if already immutable)
List<String> e = Arrays.asList("x", "y"); // fixed-size VIEW over an array: set OK, add/remove throw
List<String> f = Collections.nCopies(3, "-"); // immutable [-, -, -]
List<String> g = Collections.emptyList(); // immutable, typed empty — or List.of()
List<String> h = stream.toList(); // Java 16: unmodifiable; collect(toList()) is ArrayList in practice
The two traps: List.of throws on null elements and on any mutation; Arrays.asList is backed by the array (writes go through both ways) and throws on size changes. Wrap either in new ArrayList<>(...) when you need a mutable list.
Iterating and modifying
for (String s : xs) if (s.isEmpty()) xs.remove(s); // ConcurrentModificationException (usually)
xs.removeIf(String::isEmpty); // right
for (Iterator<String> it = xs.iterator(); it.hasNext(); ) { if (it.next().isEmpty()) it.remove(); } // also right
for (int i = xs.size() - 1; i >= 0; i--) if (xs.get(i).isEmpty()) xs.remove(i); // backwards index loop: right
ListIterator adds hasPrevious/previous, set, add and nextIndex — the tool for editing a list in place while walking it.
Equality and hashing
Two lists are equals when they have the same elements in the same order, whatever their classes (new ArrayList<>(List.of(1)).equals(new LinkedList<>(List.of(1))) is true). hashCode is defined from the elements, so a list can be a map key — as long as it is not mutated afterwards. Prefer immutable List.of for keys.
Performance habits
- Pre-size when you know the count; avoid
add(0, x)in loops (useArrayDequeor build reversed andCollections.reverse). contains/indexOfare linear: for many membership tests, put the elements in aHashSet.- Iterate
LinkedListwith for-each, neverget(i)in a loop (O(n²)). toArray(new T[0])is as fast as pre-sizing on modern JVMs and simpler.List<Integer>boxes; for numeric bulk data useint[]or a primitive-specialised library.
Interview angle
- "When would you use
LinkedList?" Almost never; when you need O(1) insert/remove via an iterator in the middle, or as aDeque— butArrayDequeis faster for that. - "How does
ArrayListgrow?" By 1.5× when full, copying the array; amortised O(1) per add. - "
list.remove(1)on aList<Integer>?" Removes index 1 — theintoverload wins. - "Is
subLista copy?" No — a view; structural changes to the parent invalidate it. - "
Arrays.asListvsList.of?" Fixed-size view over the array (writes through, allows null) vs immutable, null-hostile copy.
Key takeaways
ArrayList: array-backed, O(1) get/append, O(n) insert at front; pre-size when you can.LinkedList: rarely the right list.remove(int)vsremove(Object);subListis a live view;Arrays.asListis fixed-size;List.ofis immutable and rejects nulls.- Remove during iteration with
removeIf, anIterator, or a backwards index loop. - Lists compare and hash by contents in order.
Common questions
How does ArrayList grow in Java?
When its backing array is full, ArrayList allocates a new array 1.5 times larger and copies the elements across. Spread over many adds, each append costs O(1) amortised. Pre-size with new ArrayList<>(n) when the count is known to avoid the copies.
Why does list.remove(1) on a List<Integer> remove the wrong element?
An int argument matches remove(int index) exactly, so it removes the element at index 1, not the value 1. To remove by value, box it: list.remove(Integer.valueOf(1)) calls remove(Object).
What is the difference between Arrays.asList and List.of?
Arrays.asList returns a fixed-size view over an array: set writes through to the array, add and remove throw, and nulls are allowed. List.of returns an immutable list that rejects nulls and every change. Wrap either in new ArrayList<>(…) for a mutable list.
Is subList a copy in Java?
No — subList returns a view backed by the original list, so changes write through, and list.subList(1, 4).clear() removes that range. A later structural change to the parent invalidates the view; copy with new ArrayList<>(list.subList(1, 4)) when you need independence.
When should you use LinkedList in Java?
Almost never. Its one real advantage is O(1) insertion or removal at a position already held by an iterator, which is rarely the bottleneck. For a stack or queue ArrayDeque is faster, and for a list ArrayList is.
Exercises
List surgery
Read an integer n and n integers into an ArrayList<Integer>. Then read an integer v and do, in order: remove the element at index 1 (if it exists); remove the first occurrence of the value v (using Integer.valueOf); replace every remaining element with its double via replaceAll; clear the sublist [0, 1) if the list has at least 2 elements; finally removeIf every element greater than 100. Print the list with toString after each of the five steps.
Example: 4 then 5 6 7 60, v = 7 →
[5, 7, 60]
[5, 60]
[10, 120]
[120]
[]Merge two sorted lists
Read two sorted integer lists (n values, then m values). Merge them into one sorted ArrayList in O(n + m) with two indexes — no sorting call. Print merged=<list>, then middle=<merged.subList(1, size - 1)> when the merged list has at least 3 elements (else middle=[]), and indexOf7=<merged.indexOf(7)>.
Example: 3 then 1 4 9, 2 then 3 7 →
merged=[1, 3, 4, 7, 9]
middle=[3, 4, 7]
indexOf7=3In this module: Collections framework
- The collections framework — the map and how to choose
- Lists — ArrayList, LinkedList and the List API (this lesson)
- Sets — HashSet, LinkedHashSet and TreeSet
- Maps — HashMap internals, TreeMap, LinkedHashMap and the modern API
- Queues, deques and PriorityQueue
- Sorting, Comparable and Comparator in depth
- Iterators, fail-fast behaviour and safe modification
- Immutable collections and the utility toolkit
- Checkpoint — Collections framework
← The collections framework — the map and how to choose · Sets — HashSet, LinkedHashSet and TreeSet →