LinkedList in Java
Explore Java's doubly-linked list: insertion and removal performance, when LinkedList beats ArrayList, and memory trade-offs.
LinkedList in Java
LinkedList in Java is a doubly-linked list implementation of the List and Deque interfaces. Each element is stored in a node containing a value and pointers to both the previous and next nodes. This structure makes insertions and removals at arbitrary positions extremely cheap when you have a reference to the relevant node.
Introduction
LinkedList is a doubly-linked list implementation — each node stores its value plus pointers to both the previous and next nodes. This structure makes insertions and removals at arbitrary positions extremely cheap when you already have a reference to the neighboring node: just reconnect two pointers in O(1), no shifting required. At the head and tail, where the list maintains direct references, operations are always O(1) regardless of list size. This makes LinkedList the natural choice for queue and deque implementations where you frequently add or remove from both ends.
The tradeoff is hidden in the name: linked means pointer-chased. Each node lives separately in heap memory, connected by pointers rather than stored contiguously. The CPU cannot prefetch the next node because it does not know where it is until it dereferences the current node’s pointer. For sequential traversal — the most common iteration pattern — ArrayList is 2-5x faster in practice simply because its elements sit in contiguous memory and the CPU prefetches efficiently. Random access by index is O(n) for LinkedList versus O(1) for ArrayList — a catastrophic difference for index-heavy access patterns.
This post covers the internal node structure and pointer mechanics, the O(1) operations at head and tail via Deque methods, the failure scenarios (empty list exceptions, concurrent modification, null handling), and the performance trade-offs that determine when LinkedList beats ArrayList and when it loses decisively.
When to Use LinkedList
Use LinkedList when:
- You frequently insert or delete elements at arbitrary positions or at the head/tail
- You need a
Dequeimplementation (FIFO/BFIFO operations) with O(1) performance - You do not need random access by index very often
- You are building data structures like stacks, queues, or adjacency lists
Do not use LinkedList when:
- You frequently access elements by index (use
ArrayList) - You scan through elements in order (ArrayList has better cache locality)
- You need thread-safe operations
- Memory is constrained — each node carries two extra pointer fields
Internal Structure
LinkedList uses a static inner Node class with forward and backward pointers:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
The list maintains first and last pointers (for O(1) head/tail access) and tracks size.
Mermaid Diagram: LinkedList Node Structure
graph LR
A["Node prev=null<br/>item=A<br/>next=Node B"] --> B["Node prev=Node A<br/>item=B<br/>next=Node C"]
B --> C["Node prev=Node B<br/>item=C<br/>next=null"]
A --> D["null (head)"]
C --> E["null (tail)"]
Failure Scenarios
| Scenario | Cause | Result |
|---|---|---|
NullPointerException | Adding null when list does not permit nulls | Runtime crash |
NoSuchElementException | Calling getFirst() / pop() on empty list | Runtime crash |
ConcurrentModificationException | Modifying during iteration | Runtime crash |
| Memory overhead | Very large lists with many nodes | OutOfMemoryError from GC pressure |
Trade-Off Table
| Aspect | LinkedList | ArrayList |
|---|---|---|
| Random access | O(n) — must traverse | O(1) |
| Insertion at head | O(1) | O(n) — shift all elements |
| Insertion at tail | O(1) | O(1) amortized |
| Insertion at middle | O(n) to find position + O(1) to insert | O(n) to find + O(n) to shift |
| Memory per element | O(n) overhead — 2 pointers + value | O(1) — just value |
| Cache locality | Poor — nodes scattered | Excellent — contiguous |
| Iterator invalidation | Only on structural changes at that position | Only on structural changes at that position |
Code Snippets
Deque Operations
LinkedList<String> queue = new LinkedList<>();
queue.addLast("task1"); // enqueue
queue.addLast("task2");
queue.addFirst("urgent"); // push front
String first = queue.removeFirst(); // "urgent" — O(1)
String next = queue.removeFirst(); // "task1" — O(1)
Removing During Iteration
LinkedList<Integer> nums = new LinkedList<>(List.of(1, 2, 3, 4, 5));
ListIterator<Integer> it = nums.listIterator();
while (it.hasNext()) {
if (it.next() % 2 == 0) {
it.remove(); // Safe — iterator maintains position
}
}
// nums = [1, 3, 5]
Converting to Array
LinkedList<String> items = new LinkedList<>(List.of("a", "b", "c"));
String[] array = items.toArray(new String[0]); // Zero-length array leverages ArrayList's grow strategy
Observability Checklist
- Track
LinkedListsize to detect unbounded growth - Monitor iteration time in hot paths — O(n) scans are often misidentified as slow network calls
- Profile memory usage — each node adds ~32 bytes overhead on a 64-bit JVM
- Check for repeated
list.size() == 0checks in loops — consider explicitisEmpty()for clarity - Monitor GC frequency for large linked lists — nodes are individually garbage-collectable unlike array padding
Security Notes
LinkedListis not thread-safe; useConcurrentLinkedDequefor concurrent access- Serializing a
LinkedListcan be expensive — each node is serialized independently - Avoid storing sensitive data in nodes that may persist in memory after the list is cleared; unlike arrays, there is no efficient way to zero out node contents
Common Pitfalls / Anti-Patterns
- Assuming O(1) insertion: Insertion is O(1) only if you already have a reference to the node. Finding the node requires O(n) traversal from the nearest end.
- Memory overhead: A
LinkedListof 1 millionIntegerobjects requires ~48MB (3 pointers + object header per node) vs ~4MB for anArrayListof the same elements. - Reverse iteration: Use
list descendingIterator()for efficient tail-to-head traversal pollFirst()vsremoveFirst():pollFirst()returnsnullon empty list;removeFirst()throwsNoSuchElementExceptionaddFirst()vspush(): Both insert at head, butpush()throwsNoSuchElementExceptionif insertion fails, whileaddFirst()returnsboolean
Quick Recap
LinkedListis a doubly-linked list with O(1) insertions/removals at known positions (head/tail)- Random access is O(n) — do not use LinkedList when index-based access is frequent
- Implements both
ListandDeque— use as a queue, stack, or list - Each node stores two pointers in addition to the value — higher memory overhead than ArrayList
- Cache-unfriendly due to pointer chasing — ArrayList is faster for sequential scans
Interview Questions
::: info
The following .qa-card components contain typical interview questions you may encounter. Reviewing these will help reinforce key concepts.
:::
Model Answer: "**O(n)** for finding the insertion point plus O(1) for the actual pointer manipulation. Unlike ArrayList where finding the position is O(1) but shifting elements is O(n), LinkedList's cost is all in finding the right position. If you have a `ListIterator` already positioned, insertion is O(1)."
Model Answer: "LinkedList nodes are scattered throughout the heap, connected by pointers. The CPU cannot efficiently prefetch the next node because it does not know where it is until dereferencing the current pointer. ArrayList's contiguous memory layout lets the CPU prefetch entire cache lines, making sequential scans 2-5x faster in practice."
Model Answer: "`LinkedList` implements `Deque
Model Answer: "`NoSuchElementException` is thrown. Use `pollFirst()` instead if you want `null` returned on an empty list rather than an exception. This distinction is important when processing input that may be empty."
Model Answer: "Each `LinkedList` node stores: the element (object header + fields), a `prev` pointer (8 bytes on 64-bit JVM), and a `next` pointer (8 bytes). Plus the `Node` object header (~12 bytes). In total, each element adds roughly 32-40 bytes of overhead beyond the element itself. An `ArrayList` element in the backing array has zero per-element overhead."
Model Answer: "Both remove and return the first element. However, `pollFirst()` returns `null` when the list is empty, while `removeFirst()` throws `NoSuchElementException`. Similarly, `peekFirst()` returns `null` on empty, and `getFirst()` throws. Use `poll*` when empty is a valid/expected state."
Model Answer: "LinkedList iterators are **fail-fast** — they throw `ConcurrentModificationException` on any structural modification, whether from the same thread or another. The only exception is iterator-local changes made through that specific iterator's own `remove()` method, which updates `expectedModCount` as it proceeds."
Model Answer: "`descendingIterator()` returns an iterator starting at the last node and moving backward using `previous()`. It is implemented as a `ListIterator` wrapped around `listIterator(size())` — essentially positioning the cursor at the end and iterating in reverse."
Model Answer: "Unlike HashMap, LinkedList does **not** convert to a tree structure for large sizes. It remains a doubly-linked list regardless of size. Each node is always a `Node
Model Answer: "**O(n)** — linear search. LinkedList has no index-based lookup optimization for arbitrary access. Every `contains()` call traverses from the head (or tail if the element is likely near the end) until the element is found or the list ends."
Model Answer: "LinkedList implements `Queue` with `offer()` (add at tail), `poll()` (remove from head), `peek()` (view head). For `Deque`, it additionally provides `offerFirst()`/`pollFirst()`/`peekFirst()` and `offerLast()`/`pollLast()`/`peekLast()` for O(1) operations at both ends."
Model Answer: "Both remove and return the first element. `remove()` throws `NoSuchElementException` when the list is empty. `poll()` returns `null` when empty. Similarly, `getFirst()` throws on empty, while `peekFirst()` returns `null`. Use `poll`/`peek` when empty is a valid expected state."
Model Answer: "LinkedList provides O(1) add at both head and tail, but each insertion requires allocating a new node object (header + two pointers). For tail-add-heavy workloads where you don't need index-based access, `ArrayDeque` is typically better due to contiguous memory and no per-element allocation overhead."
Model Answer: "`listIterator(index)` returns a `ListIterator` positioned at the element at that index. It traverses from the nearest end (head if index < size/2, else tail) to reach the position. This is O(n) for positioning but then enables O(1) insertions/removals at that iterator's location."
Model Answer: "Both provide O(1) head/tail operations. LinkedList requires per-element node allocation and pointer overhead. ArrayDeque uses a contiguous circular buffer with O(1) amortized operations and better cache locality. For pure queue/deque use cases without index-based access, `ArrayDeque` is faster in practice."
Model Answer: "Yes, when you remove an element from LinkedList, the node becomes eligible for garbage collection once no references to it exist. Unlike arrays where removal leaves gaps, LinkedList nodes are individually reclaimed by the GC. However, the node's element (if mutable and referenced elsewhere) may still be alive."
Model Answer: "Each node requires approximately 32-40 bytes overhead (object header + element + two 8-byte pointers). For 1 million elements, this is ~40MB overhead plus the element storage. The pointer-based structure is also cache-unfriendly, causing more CPU cache misses during traversal compared to contiguous array storage."
Model Answer: "`peekFirst()` returns `null` if the list is empty; `getFirst()` throws `NoSuchElementException`. Similarly, `peek()` and `element()` have the same distinction for Queue. Use `peek*` when empty is a valid expected state; use `get*` when empty indicates an error condition."
Model Answer: "`addAll()` with an index inserts elements at that position, requiring traversal to find the position (O(n)) plus node allocation for each element. Without an index, `addAll()` appends at the tail in O(1) amortized per element. Each element requires a new node allocation and pointer updates."
Model Answer: "Both can implement a stack via `push()`/`pop()`/`peek()`. LinkedList uses O(1) node allocation for each push with pointer updates, while ArrayDeque uses a contiguous circular buffer with O(1) amortized operations and better cache locality. ArrayDeque is faster in practice for stack workloads and uses less memory. LinkedList's advantage is O(1) removal from both ends and the ability to use it as a double-ended queue, but for pure stack semantics ArrayDeque is the preferred choice."
Further Reading
- Oracle LinkedList Documentation — Official API documentation
- Baeldung: LinkedList vs ArrayList — Detailed performance comparison with benchmarks
- How LinkedList Works Internally — Step-by-step explanation of node structure and operations
- When to Use LinkedList — Use cases where LinkedList outperforms ArrayList
Conclusion
LinkedList excels at O(1) insertions and deletions at known positions — particularly at the head and tail. Its doubly-linked structure means no element shifting, which is its primary advantage over ArrayList. The cost is paid in memory overhead (two pointers per node) and poor cache locality during traversal.
The deciding factor between LinkedList and ArrayList is usually access pattern rather than insertion frequency. If your code accesses elements by index more often than it inserts at arbitrary positions, ArrayList wins. LinkedList also implements Deque, making it a solid choice when you need queue or stack behavior with O(1) operations at both ends.
LinkedList pairs naturally with Queue and Deque for breadth-first traversal and task scheduling. For sorted unique collections, TreeSet offers O(log n) operations with ordering guarantees instead of pointer chasing.
Category
Related Posts
Abstract Classes in Java
Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.
Arithmetic Operators in Java
Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.
Array Basics in Java
Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.