LinkedList in Java

Explore Java's doubly-linked list: insertion and removal performance, when LinkedList beats ArrayList, and memory trade-offs.

published: reading time: 13 min read author: GeekWorkBench

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 Deque implementation (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

ScenarioCauseResult
NullPointerExceptionAdding null when list does not permit nullsRuntime crash
NoSuchElementExceptionCalling getFirst() / pop() on empty listRuntime crash
ConcurrentModificationExceptionModifying during iterationRuntime crash
Memory overheadVery large lists with many nodesOutOfMemoryError from GC pressure

Trade-Off Table

AspectLinkedListArrayList
Random accessO(n) — must traverseO(1)
Insertion at headO(1)O(n) — shift all elements
Insertion at tailO(1)O(1) amortized
Insertion at middleO(n) to find position + O(1) to insertO(n) to find + O(n) to shift
Memory per elementO(n) overhead — 2 pointers + valueO(1) — just value
Cache localityPoor — nodes scatteredExcellent — contiguous
Iterator invalidationOnly on structural changes at that positionOnly 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 LinkedList size 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() == 0 checks in loops — consider explicit isEmpty() for clarity
  • Monitor GC frequency for large linked lists — nodes are individually garbage-collectable unlike array padding

Security Notes

  • LinkedList is not thread-safe; use ConcurrentLinkedDeque for concurrent access
  • Serializing a LinkedList can 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

  1. 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.
  2. Memory overhead: A LinkedList of 1 million Integer objects requires ~48MB (3 pointers + object header per node) vs ~4MB for an ArrayList of the same elements.
  3. Reverse iteration: Use list descendingIterator() for efficient tail-to-head traversal
  4. pollFirst() vs removeFirst(): pollFirst() returns null on empty list; removeFirst() throws NoSuchElementException
  5. addFirst() vs push(): Both insert at head, but push() throws NoSuchElementException if insertion fails, while addFirst() returns boolean

Quick Recap

  • LinkedList is 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 List and Deque — 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. :::

1. What is the time complexity of inserting an element at the middle of a LinkedList?

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)."

2. Why is LinkedList generally slower than ArrayList for sequential iteration?

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."

3. How does LinkedList implement the Deque interface?

Model Answer: "`LinkedList` implements `Deque` with O(1) methods: `addFirst()`, `addLast()`, `removeFirst()`, `removeLast()`, `getFirst()`, `getLast()`, plus stack methods `push()` and `pop()`. All these operations touch only the `first` and `last` node references — no traversal needed."

4. What happens when you call `removeFirst()` on an empty LinkedList?

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."

5. How much extra memory does each LinkedList node consume compared to an ArrayList element?

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."

6. What is the difference between `pollFirst()` and `removeFirst()`?

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."

7. Why is LinkedList's iterator not fail-safe when structural changes happen via another iterator?

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."

8. How does descendingIterator() work in LinkedList?

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."

9. When does LinkedList convert to a tree structure?

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` with prev/next pointers. If you need O(log n) operations on ordered data, use `TreeSet` or `TreeMap` instead."

10. What is the time complexity of checking if a LinkedList contains a specific element?

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."

11. How does LinkedList implement the Queue interface's FIFO operations?

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."

12. What is the difference between `remove()` and `poll()` in LinkedList?

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."

13. How does LinkedList perform in a tail-add-heavy workload?

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."

14. How does `listIterator(index)` work in LinkedList?

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."

15. What is the difference between LinkedList and ArrayDeque for queue operations?

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."

16. Can LinkedList's node structure be garbage collected when elements are removed?

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."

17. How does LinkedList handle very large lists in terms of memory allocation?

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."

18. What is the difference between `peekFirst()` and `getFirst()` in LinkedList?

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."

19. How does LinkedList's `addAll()` method work efficiency-wise?

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."

20. What is the difference between using LinkedList as a Stack versus ArrayDeque for stack operations?

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

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.

#java-abstract-classes #java #java-fundamentals

Arithmetic Operators in Java

Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.

#java-arithmetic-operators #java #java-fundamentals

Array Basics in Java

Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.

#java-array-basics #java #java-fundamentals