Memory and implementation
Memory and implementation details of linked lists in Java: object references, dummy and sentinel nodes, how unlinked nodes are garbage collected, and the real trade-offs between arrays and linked lists.
Learning goals
- Explain how Java references make linked lists work
- Use dummy and sentinel nodes to simplify code
- Explain what the garbage collector does with unlinked nodes
- Choose between an array and a linked list
Prerequisites
Java object references
In Java, a Node variable holds a reference to a heap object, never the object itself, and next is just another reference. That is why linked lists need no pointer arithmetic and cannot corrupt memory: a reference is either null or points at a live node. Passing a node to a method passes the reference by value, so the method can change the node's fields but cannot change which node the caller's variable points to.
This is also why methods that may change the head return the new head.
1static void setValue(Node n) { n.val = 99; } // changes the caller's node2static void replace(Node n) { n = new Node(1); } // only rebinds the local copy of the reference3static Node insertFront(Node head, int v) { Node n = new Node(v); n.next = head; return n; } // return the new head
Dummy node
A dummy node is a temporary extra node placed before the real head. It gives every real node, including the first, a predecessor, so insertions and deletions at the head need no special case, and a builder can append to tail without checking for an empty result. Return dummy.next at the end. It costs one allocation and removes a whole class of null bugs.
The merge, partition and swap-pairs examples all use one.
1static Node removeAll(Node head, int x) {2Node dummy = new Node(0); dummy.next = head;3for (Node prev = dummy; prev.next != null; ) {4if (prev.next.val == x) prev.next = prev.next.next; // works for the head too5else prev = prev.next;6}7return dummy.next;8}
Sentinel
A sentinel is a permanent dummy node that stays in the list: head and tail sentinels in a doubly linked list, or a sentinel that terminates a search. A sentinel at the end of a list lets you write while (cur.val != x) cur = cur.next without a null check, by storing the search value in the sentinel first. The data structure then has no empty-list special cases.
The difference from a dummy: a sentinel is part of the structure, a dummy is local scaffolding.
1static boolean containsWithSentinel(Node head, Node sentinel, int x) { // the list ends at the sentinel2sentinel.val = x; // guarantees the loop stops3Node cur = head;4while (cur.val != x) cur = cur.next;5return cur != sentinel; // found a real node, not the sentinel6}
GC of unlinked nodes
Java has no delete. When you unlink a node and nothing references it any more, it is unreachable and the garbage collector reclaims it, automatically and at an unspecified time. A node is still alive if anything still references it, including a stale next of the removed node itself pointing into the list. Clearing the removed node's next is not required for correctness (the collector handles chains), but it avoids keeping a long tail alive through a single stray reference.
A leak happens when you keep references you no longer need, for example in a cache.
1static Node removeFirst(Node head) {2if (head == null) return null;3Node next = head.next;4head.next = null; // help the GC: the old head no longer pins the rest of the list5return next;6}
Array vs linked list trade-offs
Arrays win on indexed access, memory per element, cache behaviour and iteration speed. Linked lists win only when you already hold a node and need O(1) insertion or removal at that position (LRU caches, splicing, queues with O(1) ends), or when elements are huge and must not move. In practice ArrayList and ArrayDeque beat LinkedList for almost every workload, even with many insertions, because the cost of cache misses and allocation dominates.
Compare reading element k in both structures.
| Operation | ArrayList | LinkedList |
|---|---|---|
| get(i) | O(1) | O(n) |
| add at the end | amortised O(1) | O(1) |
| add at index i (position known) | O(n) shift | O(1) after reaching the node (O(n) to reach it) |
| remove via iterator | O(n) shift | O(1) |
| Memory per element | reference only (4 bytes) | node object (24 to 32 bytes) |
| Cache behaviour | contiguous | scattered |
Reading element k: array vs linked list
Interactive · edit the inputarray memory (value, address)
The 7 values sit in consecutive memory cells, 4 bytes apart, starting at address 1000.
1static int getFromArray(int[] arr, int k) {2return arr[k]; // address = base + k * 4: one memory read3}45static int getFromList(Node head, int k) {6Node cur = head;7for (int i = 0; i < k; i++) {8cur = cur.next; // pointer chase: one read per hop9}10return cur.value;11}
Live state
k
5
base
1000
Execution trace (dry run table)Show 3 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | k=5 base=1000 | The 7 values sit in consecutive memory cells, 4 bytes apart, starting at address 1000. |
| 2 | L2 | k=5 address=1020 | Address = 1000 + 5 × 4 = 1020. One multiplication and one addition give the location, no matter how large k is. |
| 3 | L2 | return=8 | return 8 with a single memory read: O(1) random access. |
Practice ladder
Now try it yourself
Easy
- Why must insertFront return the new head?
- What is the difference between a dummy node and a sentinel?
Medium
- Rewrite deleteValue using a dummy head
- Explain why a removed node can still keep part of the list alive
Advanced
- Benchmark ArrayList and LinkedList for 10⁶ random-position inserts and explain the result
Key takeaways
- References replace pointers; method calls pass references by value.
- Dummy nodes remove head special cases; sentinels do the same permanently.
- Prefer ArrayList or ArrayDeque; use linked nodes when you hold the node and need O(1) splicing.