Java Tutorial
🔍

Java LinkedList Internal Working

Java LinkedList Internal Working

Every LinkedList operation — add(), get(), remove() — works by manipulating pointers on small Node objects scattered across the heap. There is no backing array. There is no pre-allocated block. Each element lives in its own Node object that holds the element reference and two pointers: one to the next node and one to the previous. Knowing this one structural fact explains every performance characteristic of LinkedList, why get(index) is O(n) when ArrayList.get() is O(1), and why adding at either end is O(1) when adding in the middle is O(n).

What Is Inside a LinkedList?

LinkedList<E> is backed by a doubly-linked chain of Node<E> objects. The class itself holds only three fields — and no array at all.

LINKEDLIST INTERNAL FIELDS (simplified from JDK source):

  transient int         size;    ← number of elements stored
  transient Node<E>     first;   ← pointer to the HEAD node (first element)
  transient Node<E>     last;    ← pointer to the TAIL node (last element)

  private static class Node<E> {
      E         item;   ← the actual element reference
      Node<E>   next;   ← pointer to the NEXT node (or null for tail)
      Node<E>   prev;   ← pointer to the PREVIOUS node (or null for head)
  }

EMPTY LINKEDLIST:
  size  = 0
  first = null
  last  = null

AFTER add("A"), add("B"), add("C"):

  LinkedList object:
    first ────────────────┐
    last ─────────────────────────────────────┐
    size = 3              │                   │
                          ▼                   ▼
         null ← [prev|"A"|next] ↔ [prev|"B"|next] ↔ [prev|"C"|next] → null

  Each Node object lives at its own heap address.
  first.next = B's node,  first.prev = null
  last.prev  = B's node,  last.next  = null
  B.prev     = A's node,  B.next     = C's node

  NAVIGATION:
    HEAD → TAIL: first.next.next... (forward traversal)
    TAIL → HEAD: last.prev.prev...  (backward traversal)
    Both directions are O(1) per step — two pointers per node.

Basic Overview — How Each Core Method Works

METHOD              WHAT HAPPENS INTERNALLY                        COMPLEXITY
─────────────────────────────────────────────────────────────────────────────
add(e)              linkLast(e): allocate new Node, wire to tail    O(1)
addFirst(e)         linkFirst(e): allocate new Node, wire to head   O(1)
add(index, e)       locate node at index via traversal, then link   O(n)
get(index)          traverse from head or tail to index             O(n)
set(index, e)       traverse to index, update node.item             O(n)
remove()            unlinkFirst(): rewire head pointer               O(1)
removeLast()        unlinkLast(): rewire tail pointer                O(1)
remove(index)       traverse to node, then unlink it                O(n)
remove(object)      linear scan for node, then unlink               O(n)
contains(object)    linear scan — indexOf(obj) >= 0                 O(n)
size()              return size field                                O(1)
isEmpty()           return size == 0                                 O(1)
peek()              first == null ? null : first.item                O(1)
peekLast()          last == null ? null : last.item                  O(1)
poll()              unlinkFirst() if not empty                       O(1)
pollLast()          unlinkLast() if not empty                        O(1)
push(e)             addFirst(e) — LIFO stack operation               O(1)
pop()               removeFirst()                                    O(1)
offer(e)            addLast(e) — FIFO queue operation                O(1)

KEY RULE: The only O(1) operations are at the two ENDS (head and tail).
          Everything requiring an index position in the MIDDLE is O(n).

When to Use LinkedList

USE LinkedList WHEN:
  1. Frequent O(1) insertions and removals at BOTH ends
     — FIFO queue: offer() at tail, poll() at head
     — LIFO stack: push() at head, pop() from head
     — Double-ended sliding window, work-stealing scheduler

  2. Both Deque AND List interfaces are needed on the same variable
     — addFirst(), addLast(), getFirst(), getLast()
     — AND get(index), subList(), listIterator()
     — Only LinkedList implements both simultaneously

  3. Frequent insertions/deletions in the MIDDLE of a large list
     IF you already hold a reference to the node via an iterator
     — iterator.remove() is O(1) after traversal — no shifting

USE ArrayList INSTEAD WHEN (which is most situations):
  - Random access by index is needed at all — get(i) is O(n) on LinkedList
  - Sequential iteration without removal is the primary operation
    (ArrayList's contiguous memory is CPU-cache-friendly; LinkedList nodes are scattered)
  - Memory efficiency matters — each LinkedList Node costs ~24 bytes extra
    (header + item + next + prev references) vs ArrayList's 4 bytes per reference

DO NOT USE LinkedList WHEN:
  - You think "it is better for insertions" without measuring
    For most real lists, ArrayList's amortised O(1) append and O(n) shift
    beats LinkedList's O(n) traversal + O(1) link for mid-list inserts
    because ArrayList's shift is a single System.arraycopy (native, SIMD-optimised)
    while LinkedList must traverse n/2 nodes before it can insert anything

How LinkedList Works Internally

Node Allocation and the Head/Tail Pointers

Every add() call allocates a new Node<E> object on the heap — no pre-allocation, no resizing. The first and last fields in the LinkedList object are the only two fixed pointers. Everything else is reached by traversing the chain.

STEP-BY-STEP: add("A"), add("B"), add("C")

AFTER add("A"):  [linkLast("A")]
  Node_A = new Node<>(prev=null, item="A", next=null)
  first  = Node_A
  last   = Node_A
  size   = 1

  Heap:  LinkedList{ first→Node_A, last→Node_A, size=1 }
         Node_A{ prev=null, "A", next=null }

AFTER add("B"):  [linkLast("B")]
  Node_B = new Node<>(prev=Node_A, item="B", next=null)
  Node_A.next = Node_B   ← wire the existing tail's next to new node
  last        = Node_B   ← move the tail pointer
  size        = 2

  Heap:  LinkedList{ first→Node_A, last→Node_B, size=2 }
         Node_A{ prev=null, "A", next→Node_B }
         Node_B{ prev→Node_A, "B", next=null }

AFTER add("C"):  [linkLast("C")]
  Node_C = new Node<>(prev=Node_B, item="C", next=null)
  Node_B.next = Node_C
  last        = Node_C
  size        = 3

  Heap:  LinkedList{ first→Node_A, last→Node_C, size=3 }
         Node_A{ prev=null,    "A", next→Node_B }
         Node_B{ prev→Node_A,  "B", next→Node_C }
         Node_C{ prev→Node_B,  "C", next=null   }

NO ARRAY. NO RESIZE. Each node is a separate heap object.
Adding 1,000 elements allocates exactly 1,000 Node objects.

get(index) — Bidirectional Traversal

LinkedList cannot jump to a position by index — there is no array to index into. Instead, it decides whether to start from first (head) or last (tail) based on whether the index is in the first or second half of the list.

get(index) IMPLEMENTATION:
  if (index < (size >> 1)) {
      Node<E> x = first;
      for (int i = 0; i < index; i++)
          x = x.next;            ← walk forward from head
      return x;
  } else {
      Node<E> x = last;
      for (int i = size - 1; i > index; i--)
          x = x.prev;            ← walk backward from tail
      return x;
  }

EXAMPLE: get(2) on list ["A","B","C","D","E"] (size=5):
  index=2, size/2=2.5 → index 2 is NOT < 2 (false)
  Walk backward from tail:
    x = Node_E (i=4)
    x = Node_D (i=3)
    x = Node_C (i=2)  ← stop
  return Node_C.item = "C"

EXAMPLE: get(1) on list ["A","B","C","D","E"] (size=5):
  index=1, 1 < 2.5 → true
  Walk forward from head:
    x = Node_A (start)
    x = Node_B (i=1)  ← stop
  return Node_B.item = "B"

WHY THIS MATTERS:
  Even with the bidirectional optimisation, get(size/2) is O(n/2) = O(n).
  Calling get(i) in a for loop is O(n²) — each call traverses half the list.
  ALWAYS use iterator or for-each when iterating — O(n) total instead of O(n²).

add(int index, E element) — Mid-List Insertion

add(2, "X") on list ["A","B","C","D"], size=4:

  Step 1: checkPositionIndex(2) — valid: 0 <= 2 <= 4
  Step 2: if index == size → linkLast; else:
          Node_at_2 = node(2) = Node_C (traverse from head: A → B → C)
  Step 3: linkBefore("X", Node_C):
          Node_X = new Node<>(prev=Node_B, item="X", next=Node_C)
          Node_B.next = Node_X   ← B now points to X
          Node_C.prev = Node_X   ← C now points back to X
          size++

  BEFORE: null ← A ↔ B ↔ C ↔ D → null
  AFTER:  null ← A ↔ B ↔ X ↔ C ↔ D → null

  POINTER SURGERY — only 4 pointer fields changed (Node_B.next, Node_X.next,
  Node_X.prev, Node_C.prev). No elements shifted. No array copied.
  But the O(n) traversal to FIND position 2 is unavoidable.

remove(int index) — Pointer Surgery

remove(1) on list ["A","B","C","D"], size=4:

  BEFORE: null ← A ↔ B ↔ C ↔ D → null

  Step 1: traverse to node at index 1 → Node_B
  Step 2: unlink(Node_B):
          Node_A.next = Node_C   ← B's predecessor now points to B's successor
          Node_C.prev = Node_A   ← B's successor now points back to B's predecessor
          Node_B.item = null     ← help GC — clear the element reference
          Node_B.next = null     ← detach Node_B from the chain
          Node_B.prev = null     ← detach Node_B from the chain
          size--

  AFTER: null ← A ↔ C ↔ D → null
         Node_B is now unreferenced → eligible for GC

  NULL-OUT AFTER REMOVAL:
    Setting Node_B.item, Node_B.next, Node_B.prev to null is critical.
    Without these assignments, Node_B holds strong references to Node_A
    and Node_C — preventing GC even after unlink.
    ArrayList does the same — elementData[--size] = null after remove.

Core Operations with Examples

Node-Level Visibility via Reflection

Java
1// File: LinkedListInternalsDemo.java 2 3import java.lang.reflect.Field; 4import java.util.LinkedList; 5 6public class LinkedListInternalsDemo { 7 8 // Reads the 'size' field and reconstructs node chain via reflection 9 static void inspectList(LinkedList<?> list, String label) throws Exception { 10 Field sizeField = LinkedList.class.getDeclaredField("size"); 11 Field firstField = LinkedList.class.getDeclaredField("first"); 12 sizeField.setAccessible(true); 13 firstField.setAccessible(true); 14 15 int size = (int) sizeField.get(list); 16 17 Class<?> nodeClass = null; 18 for (Class<?> inner : LinkedList.class.getDeclaredClasses()) { 19 if (inner.getSimpleName().equals("Node")) { 20 nodeClass = inner; 21 break; 22 } 23 } 24 Field itemField = nodeClass.getDeclaredField("item"); 25 Field nextField = nodeClass.getDeclaredField("next"); 26 itemField.setAccessible(true); 27 nextField.setAccessible(true); 28 29 System.out.printf("%-10s size=%d nodes: ", label, size); 30 Object node = firstField.get(list); 31 StringBuilder chain = new StringBuilder("HEAD → "); 32 while (node != null) { 33 chain.append("[").append(itemField.get(node)).append("]"); 34 node = nextField.get(node); 35 if (node != null) chain.append(" ↔ "); 36 } 37 chain.append(" → TAIL"); 38 System.out.println(chain); 39 } 40 41 public static void main(String[] args) throws Exception { 42 43 LinkedList<String> list = new LinkedList<>(); 44 45 System.out.println("=== Observing node chain as elements are added ==="); 46 inspectList(list, "empty"); 47 48 list.add("Alpha"); 49 inspectList(list, "add Alpha"); 50 51 list.add("Beta"); 52 inspectList(list, "add Beta"); 53 54 list.add("Gamma"); 55 inspectList(list, "add Gamma"); 56 57 list.addFirst("Zeta"); 58 inspectList(list, "addFirst Zeta"); 59 60 list.addLast("Omega"); 61 inspectList(list, "addLast Omega"); 62 63 System.out.println(); 64 System.out.println("=== After remove operations ==="); 65 list.remove("Beta"); 66 inspectList(list, "remove Beta"); 67 68 list.removeFirst(); 69 inspectList(list, "removeFirst"); 70 71 list.removeLast(); 72 inspectList(list, "removeLast"); 73 } 74}
Output:
=== Observing node chain as elements are added ===
empty      size=0  nodes: HEAD →  → TAIL
add Alpha  size=1  nodes: HEAD → [Alpha] → TAIL
add Beta   size=2  nodes: HEAD → [Alpha] ↔ [Beta] → TAIL
add Gamma  size=3  nodes: HEAD → [Alpha] ↔ [Beta] ↔ [Gamma] → TAIL
addFirst Zeta size=4  nodes: HEAD → [Zeta] ↔ [Alpha] ↔ [Beta] ↔ [Gamma] → TAIL
addLast Omega size=5  nodes: HEAD → [Zeta] ↔ [Alpha] ↔ [Beta] ↔ [Gamma] ↔ [Omega] → TAIL

=== After remove operations ===
remove Beta  size=4  nodes: HEAD → [Zeta] ↔ [Alpha] ↔ [Gamma] ↔ [Omega] → TAIL
removeFirst  size=3  nodes: HEAD → [Alpha] ↔ [Gamma] ↔ [Omega] → TAIL
removeLast   size=2  nodes: HEAD → [Alpha] ↔ [Gamma] → TAIL

O(n) get vs O(1) Head/Tail Access

Java
1// File: LinkedListAccessDemo.java 2 3import java.util.LinkedList; 4 5public class LinkedListAccessDemo { 6 7 public static void main(String[] args) { 8 9 LinkedList<Integer> list = new LinkedList<>(); 10 for (int i = 1; i <= 8; i++) list.add(i * 10); 11 12 System.out.println("List: " + list); 13 14 // O(1) — head and tail access via dedicated pointers 15 System.out.println("\n=== O(1) head/tail operations ==="); 16 System.out.println("getFirst() : " + list.getFirst()); // no traversal 17 System.out.println("getLast() : " + list.getLast()); // no traversal 18 System.out.println("peekFirst() : " + list.peekFirst()); // null if empty 19 System.out.println("peekLast() : " + list.peekLast()); // null if empty 20 21 // O(n) — mid-list access requires traversal 22 System.out.println("\n=== O(n) indexed access ==="); 23 System.out.println("get(0) : " + list.get(0)); // walks from head: 0 steps 24 System.out.println("get(3) : " + list.get(3)); // walks from head: 3 steps 25 System.out.println("get(7) : " + list.get(7)); // walks from tail: 0 steps 26 System.out.println("get(4) : " + list.get(4)); // walks from tail: 3 steps (size/2) 27 28 System.out.println(); 29 30 // THE O(n²) TRAP — get(i) in a for loop 31 System.out.println("=== O(n²) trap: get(i) in a loop ==="); 32 System.out.print("WRONG way (O(n²)): "); 33 for (int i = 0; i < list.size(); i++) { 34 System.out.print(list.get(i) + " "); // each get(i) traverses up to n/2 nodes 35 } 36 System.out.println("← AVOID for large lists"); 37 38 // The correct O(n) way 39 System.out.print("CORRECT way (O(n)): "); 40 for (int value : list) { 41 System.out.print(value + " "); // iterator advances one node at a time 42 } 43 System.out.println("← use for-each or iterator"); 44 45 System.out.println(); 46 47 // Queue pattern — offer/poll at opposite ends: O(1) each 48 System.out.println("=== FIFO queue using LinkedList ==="); 49 LinkedList<String> queue = new LinkedList<>(); 50 queue.offer("first-in"); 51 queue.offer("second-in"); 52 queue.offer("third-in"); 53 System.out.println("Queue: " + queue); 54 System.out.println("poll(): " + queue.poll()); // removes head: first-in 55 System.out.println("poll(): " + queue.poll()); // second-in 56 System.out.println("After polls: " + queue); 57 } 58}
Output:
List: [10, 20, 30, 40, 50, 60, 70, 80]

=== O(1) head/tail operations ===
getFirst()  : 10
getLast()   : 80
peekFirst() : 10
peekLast()  : 80

=== O(n) indexed access ===
get(0)      : 10
get(3)      : 40
get(7)      : 80
get(4)      : 50

=== O(n²) trap: get(i) in a loop ===
WRONG way (O(n²)): 10 20 30 40 50 60 70 80 ← AVOID for large lists
CORRECT way (O(n)): 10 20 30 40 50 60 70 80 ← use for-each or iterator

=== FIFO queue using LinkedList ===
Queue: [first-in, second-in, third-in]
poll(): first-in
poll(): second-in
After polls: [third-in]

Iterator Correctness and Fail-Fast Behaviour

Java
1// File: LinkedListIteratorDemo.java 2 3import java.util.ConcurrentModificationException; 4import java.util.Iterator; 5import java.util.LinkedList; 6import java.util.ListIterator; 7 8public class LinkedListIteratorDemo { 9 10 public static void main(String[] args) { 11 12 LinkedList<String> steps = new LinkedList<>(); 13 steps.add("validate"); steps.add("authenticate"); 14 steps.add("process"); steps.add("notify"); steps.add("audit"); 15 16 // WRONG — structural change during for-each → CME 17 System.out.println("=== for-each with remove → CME ==="); 18 try { 19 for (String step : steps) { 20 if ("process".equals(step)) { 21 steps.remove(step); // modCount++ triggers CME on next iteration 22 } 23 } 24 } catch (ConcurrentModificationException e) { 25 System.out.println("ConcurrentModificationException thrown"); 26 } 27 28 System.out.println(); 29 30 // CORRECT — Iterator.remove() keeps modCount in sync 31 System.out.println("=== Iterator.remove() — safe O(1) node removal ==="); 32 LinkedList<String> copy = new LinkedList<>(steps); 33 Iterator<String> it = copy.iterator(); 34 while (it.hasNext()) { 35 String step = it.next(); 36 if ("audit".equals(step) || "notify".equals(step)) { 37 it.remove(); // wires prev.next and next.prev — O(1) 38 } 39 } 40 System.out.println("After it.remove(): " + copy); 41 42 System.out.println(); 43 44 // CORRECT — removeIf() 45 System.out.println("=== removeIf() — clean one-pass removal ==="); 46 LinkedList<String> copy2 = new LinkedList<>(steps); 47 copy2.removeIf(step -> step.startsWith("a")); // removes authenticate, audit 48 System.out.println("After removeIf(starts with 'a'): " + copy2); 49 50 System.out.println(); 51 52 // ListIterator — bidirectional traversal and insertion 53 System.out.println("=== ListIterator — forward + backward + add ==="); 54 LinkedList<Integer> numbers = new LinkedList<>(); 55 for (int i = 1; i <= 5; i++) numbers.add(i * 10); 56 57 ListIterator<Integer> lit = numbers.listIterator(); 58 System.out.print("Forward : "); 59 while (lit.hasNext()) System.out.print(lit.next() + " "); 60 System.out.println(); 61 62 System.out.print("Backward: "); 63 while (lit.hasPrevious()) System.out.print(lit.previous() + " "); 64 System.out.println(); 65 66 // ListIterator.add() inserts at current position — O(1) after traversal 67 lit = numbers.listIterator(2); // position cursor after index 1 (between 20 and 30) 68 lit.add(25); // inserts 25 between 20 and 30 — pointer surgery only 69 System.out.println("After lit.add(25) at position 2: " + numbers); 70 } 71}
Output:
=== for-each with remove → CME ===
ConcurrentModificationException thrown

=== Iterator.remove() — safe O(1) node removal ===
After it.remove(): [validate, authenticate, process]

=== removeIf() — clean one-pass removal ===
After removeIf(starts with 'a'): [validate, process, notify]

=== ListIterator — forward + backward + add ===
Forward : 10 20 30 40 50
Backward: 50 40 30 20 10
After lit.add(25) at position 2: [10, 20, 25, 30, 40, 50]

Real-World Example — CRED Payment Processing Pipeline

CRED's payment processing service routes payments through sequential pipeline stages — KYC check, fraud detection, bank validation, and ledger entry. New stage handlers are registered at startup and payments flow through the chain in order. Handlers are rarely added or removed mid-pipeline, but when they are (A/B testing, regional compliance toggles), the linked structure allows O(1) handler insertion at any iterator position — no stage-shifting required.

Java
1// File: PipelineStage.java 2 3@FunctionalInterface 4public interface PipelineStage { 5 boolean process(String transactionId, double amount); 6}
Java
1// File: PaymentPipeline.java 2 3import java.util.Iterator; 4import java.util.LinkedList; 5 6public class PaymentPipeline { 7 8 // LinkedList chosen because: 9 // 1. Stages execute in insertion order (first-added, first-executed) 10 // 2. Stage objects are inserted/removed at iterator positions — O(1) 11 // 3. Each stage is visited exactly once per payment — O(n) traversal acceptable 12 private final LinkedList<PipelineStage> stages = new LinkedList<>(); 13 private final String pipelineName; 14 15 public PaymentPipeline(String pipelineName) { 16 this.pipelineName = pipelineName; 17 } 18 19 public void addStage(PipelineStage stage) { 20 stages.addLast(stage); // O(1) — new node at tail 21 } 22 23 public void insertStageAtFront(PipelineStage stage) { 24 stages.addFirst(stage); // O(1) — new node at head 25 } 26 27 // Insert a stage after the nth stage using ListIterator — O(n) to position, O(1) to insert 28 public void insertStageAfter(int position, PipelineStage stage) { 29 if (position >= stages.size()) { 30 stages.addLast(stage); 31 return; 32 } 33 var lit = stages.listIterator(position + 1); 34 lit.add(stage); // O(1) pointer surgery — no other nodes shift 35 } 36 37 // Remove all stages matching a condition — iterator.remove() is O(1) per removal 38 public int removeStagesMatching(java.util.function.Predicate<PipelineStage> condition) { 39 Iterator<PipelineStage> it = stages.iterator(); 40 int removed = 0; 41 while (it.hasNext()) { 42 if (condition.test(it.next())) { 43 it.remove(); // O(1) — unlinks the current node 44 removed++; 45 } 46 } 47 return removed; 48 } 49 50 // Execute all stages for a transaction; short-circuit on failure 51 public boolean execute(String transactionId, double amount) { 52 System.out.printf("[%s] Processing txn=%s amount=Rs.%.2f%n", 53 pipelineName, transactionId, amount); 54 55 // for-each traversal is O(n) — each step advances one node 56 int stageNum = 1; 57 for (PipelineStage stage : stages) { 58 boolean passed = stage.process(transactionId, amount); 59 System.out.printf(" Stage %d: %s%n", stageNum++, passed ? "PASSED" : "FAILED"); 60 if (!passed) { 61 System.out.printf(" Pipeline halted at stage %d%n", stageNum - 1); 62 return false; 63 } 64 } 65 System.out.println(" All stages passed — txn approved"); 66 return true; 67 } 68 69 public int stageCount() { return stages.size(); } 70 71 public static void main(String[] args) { 72 73 PaymentPipeline pipeline = new PaymentPipeline("CRED-UPI"); 74 75 // Register stages — addLast() = O(1) per registration 76 pipeline.addStage((txnId, amount) -> { 77 System.out.println(" KYC check..."); 78 return amount < 200_000; // KYC limit 79 }); 80 pipeline.addStage((txnId, amount) -> { 81 System.out.println(" Fraud detection..."); 82 return !txnId.startsWith("FLAGGED"); // flag check 83 }); 84 pipeline.addStage((txnId, amount) -> { 85 System.out.println(" Bank validation..."); 86 return true; // always passes in demo 87 }); 88 pipeline.addStage((txnId, amount) -> { 89 System.out.println(" Ledger entry..."); 90 return true; 91 }); 92 93 System.out.println("Pipeline has " + pipeline.stageCount() + " stages\n"); 94 95 // Normal transaction 96 pipeline.execute("TXN-9921", 4999.0); 97 System.out.println(); 98 99 // Exceeds KYC limit 100 pipeline.execute("TXN-9922", 250_000.0); 101 System.out.println(); 102 103 // Flagged transaction 104 pipeline.execute("FLAGGED-TXN-001", 1500.0); 105 System.out.println(); 106 107 // Insert compliance stage at position 1 (after KYC, before fraud check) 108 pipeline.insertStageAfter(0, (txnId, amount) -> { 109 System.out.println(" Compliance check..."); 110 return amount <= 100_000; 111 }); 112 System.out.println("Inserted compliance stage — now " + 113 pipeline.stageCount() + " stages"); 114 System.out.println(); 115 116 pipeline.execute("TXN-9923", 8499.0); 117 } 118}
Output:
Pipeline has 4 stages

[CRED-UPI] Processing txn=TXN-9921 amount=Rs.4999.00
    KYC check...
  Stage 1: PASSED
    Fraud detection...
  Stage 2: PASSED
    Bank validation...
  Stage 3: PASSED
    Ledger entry...
  Stage 4: PASSED
  All stages passed — txn approved

[CRED-UPI] Processing txn=TXN-9922 amount=Rs.250000.00
    KYC check...
  Stage 1: FAILED
  Pipeline halted at stage 1

[CRED-UPI] Processing txn=FLAGGED-TXN-001 amount=Rs.1500.00
    KYC check...
  Stage 1: PASSED
    Fraud detection...
  Stage 2: FAILED
  Pipeline halted at stage 2

Inserted compliance stage — now 5 stages

[CRED-UPI] Processing txn=TXN-9923 amount=Rs.8499.00
    KYC check...
  Stage 1: PASSED
    Compliance check...
  Stage 2: PASSED
    Fraud detection...
  Stage 3: PASSED
    Bank validation...
  Stage 4: PASSED
    Ledger entry...
  Stage 5: PASSED
  All stages passed — txn approved

Performance Considerations

OperationLinkedListArrayListNotes
add(element)O(1)O(1) amortBoth fast at end; LinkedList never resizes
addFirst(element)O(1)O(n)LinkedList wires head; ArrayList shifts all
get(index)O(n)O(1)Critical difference — LinkedList must traverse
set(index, e)O(n)O(1)Same traversal penalty as get
remove(0)O(1)O(n)LinkedList rewires head; ArrayList shifts all
remove(index)O(n)O(n)Both traverse; LinkedList pointer swap vs array shift
contains(obj)O(n)O(n)Both linear scan
Iterator traversalO(n)O(n)LinkedList pointer-chases; ArrayList is cache-friendly
Memory per element~28 bytes (Node)~4 bytes (ref)LinkedList Node: header+item+next+prev

Cache locality is the hidden cost. LinkedList nodes are allocated on the heap at different times and typically live at scattered memory addresses. Sequential iteration follows node.next pointers to arbitrary heap locations, causing CPU cache misses on each step. ArrayList's contiguous array allows hardware prefetching — the CPU loads the next few elements into cache automatically. Benchmarks consistently show ArrayList iteration outperforming LinkedList iteration even when both are O(n), simply because of cache behaviour.

Memory footprint per element. Each Node<E> object on a 64-bit JVM with compressed references costs approximately 28 bytes: 12 bytes for the object header, 4 bytes for item, 4 bytes for next, 4 bytes for prev, and 4 bytes padding. ArrayList stores 4 bytes per element (a reference into the elementData array). For one million strings, LinkedList uses ~28 MB for nodes alone; ArrayList uses ~4 MB.

Best Practices

Declare the variable as Deque<E> when using LinkedList as a double-ended queue. Deque<String> queue = new LinkedList<>() signals intent — the caller only gets queue operations (offer(), poll(), peek(), addFirst(), addLast()). Declaring LinkedList<String> exposes everything including get(index), which will be misused if visible. Consider ArrayDeque instead — it is faster than LinkedList for pure queue/stack use because of better cache behaviour and no per-element node allocation.

Never call get(index) inside a for loop on a LinkedList. for (int i = 0; i < list.size(); i++) { list.get(i); } is O(n²). Each get(i) traverses up to size/2 nodes. For a list of 10,000 elements this is 25 million node hops instead of 10,000. Use for-each or an explicit Iterator — both advance one node per step for O(n) total.

Use ListIterator for mid-list insertions. If the insertion point is found via an iterator, listIterator.add() is O(1) pointer surgery. If the insertion point must be found by index each time, that is O(n) traversal + O(1) link — equivalent to ArrayList's O(n) shift but with more heap allocation overhead. The cases where LinkedList genuinely beats ArrayList for mid-list operations are narrow: bulk insertions at the same position during a single iterator pass.

Clear the list by assigning a new instance when GC pressure matters. list.clear() iterates the chain setting each Node's fields to null — O(n). After clear(), the LinkedList holds no nodes. But if GC is not triggered, the cleared nodes may stay in memory briefly. list = new LinkedList<>() is equivalent and allows the old nodes to become unreachable in one step.

Common Mistakes

Mistake 1 — get(index) in a for Loop

Java
1LinkedList<String> events = populateWith(100_000_events); 2 3// WRONG — O(n²): each get(i) traverses up to 50,000 nodes 4for (int i = 0; i < events.size(); i++) { 5 process(events.get(i)); // traverses from head or tail on every call 6} 7 8// CORRECT — O(n): iterator advances one node per call 9for (String event : events) { 10 process(event); // uses LinkedList's ListItr — single next() per element 11}

Mistake 2 — Using LinkedList Where ArrayList Is Faster

Java
1// WRONG assumption: "LinkedList is better for insertions" 2// For bulk appends at the END, ArrayList is equally fast (O(1) amortised) 3// For mid-list inserts by INDEX, LinkedList must traverse first — still O(n) 4// ArrayList's System.arraycopy shift is native and SIMD-optimised 5LinkedList<Order> orderBuffer = new LinkedList<>(); 6for (Order o : incomingStream) { 7 orderBuffer.add(o); // O(1) — but also O(1) on ArrayList 8} 9 10// CORRECT — ArrayList is faster for sequential processing workloads 11// because contiguous memory gives CPU prefetching benefit 12List<Order> orderBuffer2 = new ArrayList<>(); 13for (Order o : incomingStream) { 14 orderBuffer2.add(o); // O(1) amortised, cache-friendly 15}

Mistake 3 — Structural Modification During For-Each Iteration

Java
1LinkedList<String> items = new LinkedList<>(List.of("A","B","C","D")); 2 3// WRONG — list.remove() changes modCount → CME on next iteration 4for (String item : items) { 5 if ("B".equals(item)) { 6 items.remove(item); // ConcurrentModificationException 7 } 8} 9 10// CORRECT — use iterator.remove() which updates expectedModCount 11Iterator<String> it = items.iterator(); 12while (it.hasNext()) { 13 if ("B".equals(it.next())) { 14 it.remove(); // O(1) — rewires prev and next of adjacent nodes 15 } 16} 17System.out.println(items); // [A, C, D]
Output:
[A, C, D]

Mistake 4 — Choosing LinkedList When ArrayDeque Is the Real Need

Java
1// WRONG — using LinkedList for a pure stack or queue 2LinkedList<String> requestQueue = new LinkedList<>(); 3requestQueue.offer("req-1"); 4requestQueue.offer("req-2"); 5String next = requestQueue.poll(); 6 7// CORRECT — ArrayDeque is faster for queue and stack use 8// No per-element Node allocation, better cache locality 9java.util.Deque<String> requestQueue2 = new java.util.ArrayDeque<>(); 10requestQueue2.offer("req-1"); 11requestQueue2.offer("req-2"); 12String next2 = requestQueue2.poll(); // O(1), no Node allocation overhead 13 14// Choose LinkedList over ArrayDeque ONLY when: 15// — both List AND Deque interfaces are needed on the same variable 16// — null elements must be stored (ArrayDeque rejects null)

Interview Questions

Q1. What is the internal data structure of LinkedList in Java?

LinkedList<E> is backed by a doubly-linked chain of Node<E> objects. Each Node holds three fields: item (the element reference), next (pointer to the next node), and prev (pointer to the previous node). The LinkedList class itself holds only three fields: size, first (pointer to the head node), and last (pointer to the tail node). There is no backing array — elements are stored in individually allocated Node objects scattered across the heap. This means there is no resize operation: adding an element always allocates exactly one new Node.

Q2. Why is get(index) O(n) on LinkedList but O(1) on ArrayList?

ArrayList stores elements in a contiguous Object[] array and can access any position directly: elementData[index] is a single array dereference — O(1). LinkedList has no array; to reach position i, it must start at first or last and follow next or prev pointers one step at a time until the correct node is reached. Even with the bidirectional optimisation (start from the closer end), the worst case is O(n/2) = O(n). This is why calling get(i) inside a for loop on a LinkedList produces O(n²) behaviour.

Q3. Why does LinkedList use more memory than ArrayList per element?

Each LinkedList element is stored in a Node<E> object that carries three references: item, next, and prev. On a 64-bit JVM with compressed references, each Node costs approximately 28 bytes — 12 for the object header plus 4 bytes each for the three reference fields plus alignment padding. ArrayList stores a 4-byte reference per element in its contiguous Object[]. For one million elements, LinkedList uses roughly 28 MB just for nodes; ArrayList uses roughly 4 MB for the reference array. This 7:1 ratio compounds under GC pressure.

Q4. When does LinkedList's O(1) insertion actually beat ArrayList?

LinkedList's O(1) insertion advantage over ArrayList is real only in two specific cases. First, insertions at either end — addFirst() or addLast() — are O(1) on LinkedList versus O(n) for add(0, e) on ArrayList. Second, mid-list insertions via a ListIterator already positioned at the target node — the link operation is O(1) after the iterator walk. For typical mid-list inserts specified by index, LinkedList still traverses O(n) nodes to find the position, then does O(1) pointer surgery. ArrayList's O(n) System.arraycopy shift is a native SIMD-optimised bulk memory operation that is often faster in practice than the linked traversal at the same O(n) complexity.

Q5. What is the modCount mechanism and how does it cause ConcurrentModificationException?

modCount is an inherited int field from AbstractList that increments on every structural modification — add(), remove(), clear(), or any operation that changes the list's size. When iterator() is created, it captures modCount as expectedModCount. Every call to iterator.next() checks modCount == expectedModCount. If they differ, it means a structural change happened after the iterator was created, and ConcurrentModificationException is thrown. Iterator.remove() is the safe alternative — it performs the unlinking and then updates expectedModCount = modCount to keep them in sync.

Q6. How does removing a node from the middle of a LinkedList work internally?

remove(index) first traverses to the target node — O(n). Then unlink(node) executes four pointer updates: node.prev.next = node.next (the predecessor's forward pointer skips the target), node.next.prev = node.prev (the successor's backward pointer skips the target), then node.item = null, node.next = null, node.prev = null (all three fields on the removed node are nulled for GC). The null assignments are critical — without them, the unlinked Node object would still hold strong references to its neighbours, preventing garbage collection. This is the same reason ArrayList.remove() nulls the last element slot after shifting.

FAQs

What is the difference between LinkedList and ArrayList internally?

ArrayList uses a contiguous Object[] array with a logical size counter. Random access is O(1). Adding at the end is O(1) amortised. Inserting in the middle shifts elements via System.arraycopy. LinkedList uses a doubly-linked chain of Node objects. Random access requires traversal — O(n). Adding at either end is O(1) — just pointer surgery. Memory per element is ~28 bytes vs ~4 bytes for ArrayList.

Why is LinkedList iteration slower than ArrayList iteration despite both being O(n)?

Big-O notation ignores constant factors. ArrayList iteration reads from a contiguous array, allowing CPU hardware prefetching — the next several elements are loaded into L1/L2 cache automatically. LinkedList iteration follows node.next pointers to arbitrary heap addresses, causing cache misses on each step. Benchmarks consistently show ArrayList iteration outperforming LinkedList iteration by 2-5× for the same number of elements, even though both are O(n).

Does LinkedList support null elements?

Yes. LinkedList allows null elements — multiple nulls in the same list are permitted. list.contains(null) works correctly. ArrayDeque does not allow null; this is one specific case where LinkedList is the correct choice if null elements are required alongside queue or stack operations.

When should I use LinkedList over ArrayDeque for queue operations?

Use ArrayDeque for pure queue or stack operations in almost all cases — it is faster and more memory-efficient. Choose LinkedList over ArrayDeque only when you also need List interface methods on the same variable (get(index), set(index, e), subList()) or when null elements must be stored. ArrayDeque rejects null and does not implement List.

What does ListIterator give you that Iterator does not?

ListIterator<E> extends Iterator<E> with bidirectional traversal (hasPrevious(), previous()), positional information (nextIndex(), previousIndex()), forward insertion at the current cursor position (add(e) — O(1) pointer surgery on LinkedList), and in-place element replacement (set(e)). For LinkedList, ListIterator.add() is the only O(1) mid-list insertion — it inserts between the last returned element and the next, wiring the prev/next pointers without any traversal cost.

What happens when you call clear() on a LinkedList?

clear() iterates the entire chain setting each Node's item, next, and prev fields to null, then sets first = null, last = null, and size = 0. The null-out is essential — each Node holds strong references to its neighbours. Without clearing those references, detached nodes would keep each other alive in memory even after clear(). After clear(), the GC can reclaim the entire chain since no more strong references exist. The operation is O(n) — one pass through all nodes.

Summary

LinkedList<E> is a doubly-linked chain of Node objects — each holding the element, a forward pointer, and a backward pointer. The LinkedList class holds only size, first (head), and last (tail). There is no backing array, no resize, no pre-allocation. Every element lives at its own heap address.

The performance profile follows directly from this structure. Head and tail operations — addFirst(), addLast(), removeFirst(), removeLast(), peekFirst(), peekLast() — are all O(1). Everything involving a middle index requires traversal — O(n). The O(n²) get-in-loop mistake is the most common LinkedList performance bug in production code: for-each is the correct traversal pattern for any sequential processing.

For interviews: know that get(index) traverses from first or last depending on which half the index falls in, explain the Node null-out pattern after removal and why it prevents memory leaks, and name ArrayDeque as the preferred replacement for pure queue and stack use cases.

What to Read Next

TopicLink
How ArrayList uses a contiguous Object array as an alternative to LinkedList's node chainJava ArrayList →
How HashMap uses a similar node-chain approach for collision handling in each bucketJava HashMap →
How the Iterator interface enables safe traversal across both ArrayList and LinkedListJava Iterator →
How the Collections Framework organises LinkedList's interfaces and implementationsJava Collections Framework →
How Generics make LinkedList type-safe with bounded type parametersJava Generics →
Java LinkedList Internal Working | DevStackFlow