ArrayList vs LinkedList
Choosing between ArrayList and LinkedList is one of the most common decisions you’ll make when working with Java collections. Both implement the List interface, so they share the same API — but their internal structures are completely different, and those differences have real performance consequences.
The Core Difference
ArrayListstores elements in a contiguousObject[]array. Random access is instant, but inserting in the middle requires shifting elements.LinkedListstores elements as a chain of doubly-linkedNodeobjects. Insertion and deletion at any position are fast once you have a reference, but random access requires walking the chain.
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class BasicComparison {
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
List<String> linkedList = new LinkedList<>();
// Both expose identical List API
arrayList.add("Java");
linkedList.add("Java");
System.out.println(arrayList.get(0)); // Java
System.out.println(linkedList.get(0)); // Java
}
}
Output:
Java
Java
Note: Because both implement
List, you can switch between them by changing only the constructor — your calling code stays the same.
Internal Structure
ArrayList Internals
ArrayList maintains a backing Object[] elementData array with a default initial capacity of 10. When the array fills up, Java allocates a new array at 1.5× the old size (newCapacity = oldCapacity + (oldCapacity >> 1)) and copies all elements over. This is the “grow” operation — it is O(n) but happens infrequently (amortized O(1) per add).
// Simplified mental model of what ArrayList.add() looks like:
// if (size == elementData.length) grow();
// elementData[size++] = element;
Elements sit side-by-side in memory, so the CPU cache loves them — iterating an ArrayList is extremely cache-friendly.
LinkedList Internals
LinkedList is implemented as a doubly-linked list. Each element is wrapped in a private Node<E> object:
// Simplified Node structure (from JDK source)
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
LinkedList keeps references to both the first and last nodes, making operations at either end O(1). However, accessing element i in the middle requires traversing from whichever end is closer — O(n/2) = O(n).
Note:
LinkedListalso implementsDeque, so it can act as a queue or stack withaddFirst(),addLast(),removeFirst(), andremoveLast().
Performance Comparison
| Operation | ArrayList | LinkedList | Why |
|---|---|---|---|
get(i) — random access | O(1) | O(n) | Array index vs node traversal |
add(e) — append to end | O(1) amortized | O(1) | Array append vs last-node link |
add(i, e) — insert in middle | O(n) | O(n)* | Shift vs traverse then re-link |
remove(i) — remove by index | O(n) | O(n)* | Shift vs traverse then unlink |
contains(e) — linear search | O(n) | O(n) | Both scan sequentially |
| Memory per element | ~4–8 bytes | ~24–40 bytes | Object reference vs Node object |
| Cache friendliness | Excellent | Poor | Contiguous memory vs scattered nodes |
*LinkedList insert/remove is O(1) once you hold the iterator/node. Getting there is still O(n).
Tip: The O(n) middle-insertion looks the same on paper, but
ArrayListis often faster in practice for small-to-medium lists because array shifting is a highly optimised memory-copy (System.arraycopy) that benefits from CPU cache, whileLinkedListchases pointers scattered across the heap.
Code Examples
Random Access (ArrayList wins)
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class RandomAccessDemo {
public static void main(String[] args) {
List<Integer> al = new ArrayList<>();
List<Integer> ll = new LinkedList<>();
for (int i = 0; i < 100_000; i++) {
al.add(i);
ll.add(i);
}
long start = System.nanoTime();
for (int i = 0; i < 100_000; i++) al.get(i);
long alTime = System.nanoTime() - start;
start = System.nanoTime();
for (int i = 0; i < 100_000; i++) ll.get(i);
long llTime = System.nanoTime() - start;
System.out.println("ArrayList get: " + alTime / 1_000_000 + " ms");
System.out.println("LinkedList get: " + llTime / 1_000_000 + " ms");
}
}
Output (approximate):
ArrayList get: 1 ms
LinkedList get: 4500 ms
Insert at the Front (LinkedList wins)
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class FrontInsertDemo {
public static void main(String[] args) {
List<Integer> al = new ArrayList<>();
List<Integer> ll = new LinkedList<>();
long start = System.nanoTime();
for (int i = 0; i < 50_000; i++) al.add(0, i); // insert at index 0
long alTime = System.nanoTime() - start;
start = System.nanoTime();
for (int i = 0; i < 50_000; i++) ll.add(0, i); // insert at index 0
long llTime = System.nanoTime() - start;
System.out.println("ArrayList front-insert: " + alTime / 1_000_000 + " ms");
System.out.println("LinkedList front-insert: " + llTime / 1_000_000 + " ms");
}
}
Output (approximate):
ArrayList front-insert: 280 ms
LinkedList front-insert: 12 ms
Using LinkedList as a Queue
import java.util.LinkedList;
import java.util.Queue;
public class QueueDemo {
public static void main(String[] args) {
Queue<String> queue = new LinkedList<>();
queue.offer("first");
queue.offer("second");
queue.offer("third");
while (!queue.isEmpty()) {
System.out.println(queue.poll());
}
}
}
Output:
first
second
third
Tip: If you need a queue or deque,
ArrayDequeis generally faster thanLinkedListfor that use case too. Reach forLinkedListas aDequeonly when you already need it as aList.
Memory Overhead
Memory matters at scale. For a list of Integer objects:
ArrayList: stores object references — roughly 4–8 bytes per element reference, plus the backing array object overhead.LinkedList: each element is wrapped in aNodeholding three references (item,next,prev) — roughly 24–40 bytes per node depending on JVM pointer compression.
At 1 million elements, LinkedList can consume several times more memory than ArrayList.
Iterating Both Lists
Both types iterate efficiently when you use a for-each loop or an explicit Iterator, because LinkedList’s iterator moves from node to node without any index-based traversal overhead.
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedList;
public class IterationDemo {
public static void main(String[] args) {
List<String> list = new LinkedList<>();
list.add("A");
list.add("B");
list.add("C");
// Always prefer for-each or iterator over index-based loop for LinkedList
for (String s : list) {
System.out.print(s + " ");
}
}
}
Output:
A B C
Warning: Never use an index-based
for (int i = 0; i < list.size(); i++)loop on aLinkedList— eachget(i)call traverses from the head, turning O(n) iteration into O(n²) torture.
Under the Hood
ArrayList Growth Strategy
When an ArrayList grows, the JVM calls Arrays.copyOf(), which delegates to System.arraycopy() — a native method that uses memcpy-style bulk memory operations. This is why even the O(n) resize is fast in absolute terms: it is a single contiguous block copy, not a loop over individual objects.
The JIT compiler can also vectorise tight ArrayList loops using SIMD instructions. LinkedList pointer chasing is much harder to vectorise.
LinkedList Node Allocation
Every add() on a LinkedList allocates a new Node object on the heap. More allocations means more pressure on the garbage collector, more GC pauses, and more fragmented heap memory — all of which hurt throughput in high-volume scenarios.
Sublist and Iterator Invalidation
Both ArrayList and LinkedList use a modCount field to detect structural modifications during iteration. If you modify the list while iterating (outside of the iterator’s own remove() method), a ConcurrentModificationException is thrown immediately — a fail-fast behaviour that catches bugs early.
Quick Decision Guide
| You need… | Use |
|---|---|
| Fast random access by index | ArrayList |
| Frequent append to end | Either (both O(1)) |
| Frequent insert/delete at front or known position | LinkedList |
| Queue / Deque semantics | LinkedList or ArrayDeque |
| Minimum memory footprint | ArrayList |
| Iteration-heavy, rarely modified | ArrayList |
| Thread-safe list | Neither — use CopyOnWriteArrayList or Collections.synchronizedList() |
Tip: When in doubt, start with
ArrayList. It is faster for the majority of real-world workloads, uses less memory, and is friendlier to the JVM’s optimiser. Switch toLinkedListonly when profiling proves it helps.
Related Topics
- ArrayList — deep dive into ArrayList internals and all its methods
- LinkedList — LinkedList-specific methods, Deque usage, and examples
- List Interface — the common contract both classes implement
- Collections Utility Class — sorting, searching, and wrapping lists
- Iterator — safe iteration and removal over any List
- Concurrent Collections — thread-safe alternatives to ArrayList and LinkedList