SimpleLinkedList.java
import java.util.NoSuchElementException;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
/**
* A simple linked list class, implemented completely from scratch, using doubly
* linked nodes and a cool "header" node to greatly simplify insertion and
* deletions on the end of the list.
*/
public class SimpleLinkedList {
/**
* Doubly-linked node class, completely private to the List class, as clients
* don't care about the implementation of the list. This is a regular class and
* not a record because nodes are mutable.
*/
private class Node {
Object item;
Node next;
Node previous;
Node(Object item, Node next, Node previous) {
this.item = item;
this.next = next;
this.previous = previous;
}
}
/**
* The list itself maintains only a reference to its "header" node. The header
* is a node that does not store any data. Its 'next' field points to the first
* item in the list and its 'previous' field points to the last item. This makes
* all insertions and deletions uniform, even at the beginning and the end of
* the list!
*/
private Node header;
/**
* The number of items in the list, stored to make size() O(1).
*/
private int size = 0;
public SimpleLinkedList() {
// The empty list is made by pointing the header to itself.
header = new Node(null, null, null);
header.next = header.previous = header;
}
public int size() {
// The size stored as a field for easy lookup
return size;
}
public void addFirst(Object item) {
// The first node is the one right after the header.
addBefore(item, header.next);
}
public void addLast(Object item) {
// The last node is the one right before the header.
addBefore(item, header);
}
public void add(int index, Object item) {
// This one is trickier, as there is a special case. If the index is equal
// to the size, we are just adding the new element at the end. Otherwise
// we use the special nodeAt() utility to point to the existing node at the
// desired position. Adding before this node does the trick.
addBefore(item, (index == size ? header : nodeAt(index)));
}
public void remove(int index) {
// The utility methods do all the work here.
remove(nodeAt(index));
}
public Object get(int index) {
// Many thanks to the nodeAt() utility!
return nodeAt(index).item;
}
public void set(int index, Object item) {
// nodeAt() is sure cool, innit?
nodeAt(index).item = item;
}
public int indexOf(Object item) {
// Ah the old "linear search" pattern: a for-loop with a return in
// the middle. Note that to iterate the actual nodes, we start at
// header.next, since header does not hold a list item. The loop
// terminates when we get back to the header.
var index = 0;
for (var node = header.next; node != header; node = node.next) {
if (node.item.equals(item)) {
return index;
}
index++;
}
return -1;
}
public void forEach(Consumer<Object> consumer) {
// Iterate from the first node (the one after the header).
for (var node = header.next; node != header; node = node.next) {
consumer.accept(node.item);
}
}
public void take(int n) {
if (n < 0 || n > size) {
throw new IllegalArgumentException("Cannot take that many");
}
if (n == 0) {
// Unhook them all
header.next = header.previous = header;
} else {
// Make the nth element the new last one
var newLastNode = nodeAt(n - 1);
newLastNode.next = header;
header.previous = newLastNode;
}
size = n;
}
public void drop(int n) {
if (n < 0 || n > size) {
throw new IllegalArgumentException("Cannot drop that many");
}
if (n == size) {
// Unhook them all
header.next = header.previous = header;
} else {
// Make the nth element the new last one
var newFirstNode = nodeAt(n);
newFirstNode.previous = header;
header.next = newFirstNode;
}
size -= n;
}
public void reverse() {
var node = header;
// Flip the next and previous links in every node including the header.
// It's hard to get the terminating condition of the loop exactly right
// so hey just count!
for (var i = 0; i <= size(); i++) {
var oldNext = node.next;
node.next = node.previous;
node = node.previous = oldNext;
}
}
public void append(SimpleLinkedList other) {
if (this == other) {
throw new IllegalArgumentException("Cannot append to self");
}
other.forEach(this::addLast);
}
public void map(UnaryOperator<Object> f) {
// Straight iteration through list nodes
for (var node = header.next; node != header; node = node.next) {
node.item = f.apply(node.item);
}
}
public void filter(Predicate<Object> p) {
// This method is destructive so we can make use of the remove utility.
for (var node = header.next; node != header; node = node.next) {
if (!p.test(node.item)) {
remove(node);
}
}
}
public Object last() {
if (size == 0) {
throw new NoSuchElementException("No last element");
}
return header.previous.item;
}
public boolean every(Predicate<Object> p) {
for (var node = header.next; node != header; node = node.next) {
if (!p.test(node.item)) {
return false;
}
}
return true;
}
public boolean some(Predicate<Object> p) {
for (var node = header.next; node != header; node = node.next) {
if (p.test(node.item)) {
return true;
}
}
return false;
}
public static SimpleLinkedList of(Object... items) {
var list = new SimpleLinkedList();
for (var item : items) {
list.addLast(item);
}
return list;
}
/**
* Get the node at a given index.
*/
private Node nodeAt(int index) {
// The only legal indexes are 0, 1, ... size-1.
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException(index + " for size " + size);
}
// Only way to get by index in a simple linked list is to walk and
// count. There are other kinds of lists that give quicker access by
// position, but those structures are for another time.
var node = header;
for (var i = 0; i <= index; i++) {
node = node.next;
}
return node;
}
private void addBefore(Object item, Node node) {
var newNode = new Node(item, node, node.previous);
newNode.previous.next = newNode;
newNode.next.previous = newNode;
size++;
}
/**
* Removes the referenced node by making its neighbors point around it.
*/
private void remove(Node node) {
// Just in case
if (node == header) {
throw new NoSuchElementException();
}
node.previous.next = node.next;
node.next.previous = node.previous;
size--;
}
}
SimpleImmutableList.java
import java.util.NoSuchElementException;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
/**
* A simple immutable linked list, implement completely from scratch as a
* singly-linked structure. There aren't too many operations, as additional
* will be left for homework.
*
* The implementation uses a sum type because I have major null-phobia.
* Not going to pay the billion-dollar mistake, not with this one.
*/
public sealed interface SimpleImmutableList permits EmptyList, ListNode {
int size();
Object head();
SimpleImmutableList tail();
static SimpleImmutableList of(Object... items) {
SimpleImmutableList list = new EmptyList();
for (var i = items.length - 1; i >= 0; i--) {
list = new ListNode(items[i], list);
}
return list;
}
static SimpleImmutableList from(Object head, SimpleImmutableList tail) {
return new ListNode(head, tail);
}
Object at(int index);
void forEach(Consumer<Object> consumer);
SimpleImmutableList take(int n);
SimpleImmutableList drop(int n);
SimpleImmutableList reversed();
SimpleImmutableList append(SimpleImmutableList other);
SimpleImmutableList map(UnaryOperator<Object> f);
SimpleImmutableList filter(Predicate<Object> p);
Object last();
boolean every(Predicate<Object> p);
boolean some(Predicate<Object> p);
}
final record EmptyList() implements SimpleImmutableList {
public int size() {
return 0;
}
public Object head() {
throw new NoSuchElementException();
}
public SimpleImmutableList tail() {
throw new NoSuchElementException();
}
public Object at(int index) {
throw new NoSuchElementException();
}
public void forEach(Consumer<Object> consumer) {
// Intentionally empty: nothing to iterate
}
public SimpleImmutableList take(int n) {
if (n != 0) {
throw new IllegalArgumentException("Cannot take that many");
}
return this;
}
public SimpleImmutableList drop(int n) {
if (n != 0) {
throw new IllegalArgumentException("Cannot drop that many");
}
return this;
}
public SimpleImmutableList reversed() {
return this;
}
public SimpleImmutableList append(SimpleImmutableList other) {
return other;
}
public SimpleImmutableList map(UnaryOperator<Object> f) {
return this;
}
public SimpleImmutableList filter(Predicate<Object> p) {
return this;
}
public Object last() {
throw new NoSuchElementException();
}
public boolean every(Predicate<Object> p) {
return true;
}
public boolean some(Predicate<Object> p) {
return false;
}
}
final record ListNode(Object head, SimpleImmutableList tail) implements SimpleImmutableList {
public int size() {
return 1 + tail.size();
}
public Object at(int index) {
return index == 0 ? head : tail.at(index - 1);
}
public void forEach(Consumer<Object> consumer) {
consumer.accept(head);
tail.forEach(consumer);
}
public SimpleImmutableList take(int n) {
if (n < 0) {
throw new IllegalArgumentException();
}
if (n == 0) {
return new EmptyList();
}
return new ListNode(head, tail.take(n - 1));
}
public SimpleImmutableList drop(int n) {
if (n < 0) {
throw new IllegalArgumentException();
}
if (n == 0) {
return this;
}
return tail.drop(n - 1);
}
public SimpleImmutableList reversed() {
return tail.reversed().append(new ListNode(head, new EmptyList()));
}
public SimpleImmutableList append(SimpleImmutableList other) {
return new ListNode(head, tail.append(other));
}
public SimpleImmutableList map(UnaryOperator<Object> f) {
return new ListNode(f.apply(head), tail.map(f));
}
public SimpleImmutableList filter(Predicate<Object> p) {
return p.test(head) ? new ListNode(head, tail.filter(p)) : tail.filter(p);
}
public Object last() {
return tail instanceof EmptyList ? head : tail.last();
}
public boolean every(Predicate<Object> p) {
return p.test(head) && tail.every(p);
}
public boolean some(Predicate<Object> p) {
return p.test(head) || tail.some(p);
}
}