Solution: Removing by Index

In the version from the chapter, remove(index) has to check whether the target is the first node or the last node. With sentinels, every real node has a node before it and a node after it. So the same two rewiring lines work for every valid index.

Solution

It helps to first write a helper that unlinks a node we already have a reference to:

private void unlink(Node<T> target) {
  target.prev.next = target.next;
  target.next.prev = target.prev;
  size--;
}

The two rewiring lines change the links of the nodes on either side of the target. Suppose we remove the 5 from [3, 5, 7]. Before the unlink, the target is between its two neighbors:

          ┌─────┐      ┌───┐      ┌───┐      ┌───┐      ┌─────┐
head ───▶ │ ░░░ │ ◀──▶ │ 3 │ ◀──▶ │ 5 │ ◀──▶ │ 7 │ ◀──▶ │ ░░░ │ ◀─── tail
          └─────┘      └───┘      └───┘      └───┘      └─────┘
                                    ▲
                                    │
                                 target
           front                                         back
         sentinel                                      sentinel

target.prev.next = target.next makes 3 point forward to 7, and target.next.prev = target.prev makes 7 point back to 3. The target is now skipped on both sides:

          ┌─────┐      ┌───┐                 ┌───┐      ┌─────┐
head ───▶ │ ░░░ │ ◀──▶ │ 3 │ ◀─────────────▶ │ 7 │ ◀──▶ │ ░░░ │ ◀─── tail
          └─────┘      └───┘                 └───┘      └─────┘
           front                                         back
         sentinel                                      sentinel

                                  ┌───┐
                                  │ 5 │   (unlinked)
                                  └───┘

Then remove(index) composes the pieces:

public T remove(int index) {
  validateIndex(index);
  Node<T> target = node(index);
  T removed = target.value;
  unlink(target);
  return removed;
}

validateIndex throws for an out-of-range index before node(index) or unlink ever runs.

Why is the unlink safe with no branches? Because node(index) only returns real nodes, and a real node is always between two nodes. The node on either side might be a sentinel or it might be another real node, but it is never null. So target.prev and target.next can always be dereferenced.

In the chapter version, the first real node needs its own branch, because in a list without sentinels it has nothing before it. Here the node before it is the front sentinel. Removing the 3 from [3, 5, 7] looks like this before the unlink:

          ┌─────┐      ┌───┐      ┌───┐      ┌───┐      ┌─────┐
head ───▶ │ ░░░ │ ◀──▶ │ 3 │ ◀──▶ │ 5 │ ◀──▶ │ 7 │ ◀──▶ │ ░░░ │ ◀─── tail
          └─────┘      └───┘      └───┘      └───┘      └─────┘
                         ▲
                         │
                      target
           front                                         back
         sentinel                                      sentinel

target.prev is the front sentinel, not null, so the same two lines apply. After the unlink, the sentinel points across to 5:

          ┌─────┐                   ┌───┐      ┌───┐      ┌─────┐
head ───▶ │ ░░░ │ ◀───────────────▶ │ 5 │ ◀──▶ │ 7 │ ◀──▶ │ ░░░ │ ◀─── tail
          └─────┘                   └───┘      └───┘      └─────┘
           front                                           back
         sentinel                                        sentinel

                        ┌───┐
                        │ 3 │   (unlinked)
                        └───┘

The last real node is symmetric: its target.next is the back sentinel. And the only real node has the front sentinel before it and the back sentinel after it. In every case both neighbors exist, so the same two rewiring lines work with no branches.