Solution: Access and Search

The sentinels change where the traversal starts and stops.

For indexed access, index 0 is no longer at head. head is the front sentinel now, so index 0 is head.next. For search, the traversal does not stop at null. It stops when it reaches the back sentinel, tail.

Solution

First, here is node(index). It traverses from whichever end is closer, with no bounds check of its own:

private Node<T> node(int index) {
  if (index < size / 2) {
    Node<T> current = head.next;
    for (int i = 0; i < index; i++) {
      current = current.next;
    }
    return current;
  } else {
    Node<T> current = tail.prev;
    for (int i = size - 1; i > index; i--) {
      current = current.prev;
    }
    return current;
  }
}

One thing changed from the chapter version. The starting points moved: a forward traversal starts at head.next and a backward traversal starts at tail.prev, because head and tail are sentinels now and the first and last real nodes are just inside them.

The bounds check itself is unchanged from the chapter: a separate validateIndex helper, called once by every method that takes an index, before it touches node:

private void validateIndex(int index) {
  if (index < 0 || index >= size) {
    throw new IndexOutOfBoundsException();
  }
}

For a front-half index, the traversal starts at the first real node and steps forward. To reach index 2, current begins at head.next:

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

Step forward:

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

And stop at the target:

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

For a back-half index, the closer end is the back, so the traversal starts at the last real node and steps backward. To reach index 2, current begins at tail.prev:

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

Step backward to the target:

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

With the bounds check factored out, get and set each call validateIndex first:

public T get(int index) {
  validateIndex(index);
  return node(index).value;
}

public void set(int index, T value) {
  validateIndex(index);
  node(index).value = value;
}

For search, the traversal starts at the first real node and continues until it reaches the back sentinel:

public int indexOf(T value) {
  Node<T> current = head.next;
  int index = 0;
  while (current != tail) {
    if (Objects.equals(current.value, value)) {
      return index;
    }
    current = current.next;
    index++;
  }
  return -1;
}

public boolean contains(T value) {
  return indexOf(value) != -1;
}

Looking for 7, current begins at the first real node:

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

Advance:

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

Stop on the match:

          ┌─────┐      ┌───┐      ┌───┐      ┌───┐      ┌─────┐
head ───▶ │ ░░░ │ ◀──▶ │ 3 │ ◀──▶ │ 5 │ ◀──▶ │ 7 │ ◀──▶ │ ░░░ │ ◀─── tail
          └─────┘      └───┘      └───┘      └───┘      └─────┘
                                               ▲
                                               │
                                            current   (match)
           front                                         back
         sentinel                                      sentinel

The file needs:

import java.util.Objects;

The sentinels stay inside the list. get(0) returns the first real value, not the front sentinel’s null.