Singly Linked List

public class SinglyLinkedList<T> implements Iterable<T>{
    private ListNode<T> head;
    private int size;

    public SinglyLinkedList() {
        head = null;
        size = 0;
    }

    public void insertAtHead(T value) {
        ListNode<T> node = new ListNode<>(value);
        node.next = head;
        head = node;
        size++;
    }

    public void insertAtEnd(T value) {
        ListNode<T> node = new ListNode<>(value);
        if (head == null) {
            head = node;
        } else {
            var curr = head;
            while (curr.next != null) {
                curr = curr.next;
            }
            curr.next = node;
        }
        size++;
    }

    public void insertAfter(T value, T previous) {
        if (isEmpty()) {
            insertAtEnd(value);
        }
        ListNode<T> node = new ListNode<>(value);
        var curr = head;
        while (curr.next != null && curr != previous) {
            curr = curr.next;
        }
        if (curr.next == null) {
            curr.next = node;
        } else {
            var next = curr.next;
            curr.next = node;
            node.next = next;
        }
    }

    public boolean remove(T value) {
        ListNode<T> curr = head, prev = null;
        while (curr != null) {
            if (curr.value.equals(value)) {
                if (prev == null) {
                    head = curr.next;
                } else {
                    prev.next = curr.next;
                }
                size--;
                return true;
            }
            prev = curr;
            curr = curr.next;
        }
        return false;
    }

    public boolean remove(int index) {
        if (index >= size) {
            throw new IndexOutOfBoundsException(index);
        }
        int i = 0;
        ListNode<T> curr = head, prev = null;
        while (curr != null) {
            if (i == index) {
                if (prev == null) {
                    head = curr.next;
                } else {
                    prev.next = curr.next;
                }
                break;
            }
            i++;
            prev = curr;
            curr = curr.next;
        }
        size--;
        return true;
    }

    public int size() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public T get(int index) {
        if (index >= size) {
            throw new IndexOutOfBoundsException(index);
        }
        var curr = head;
        int i = 0;
        while (curr != null) {
            if (i == index) {
                break;
            }
            i++;
            curr = curr.next;
        }
        return Objects.requireNonNull(curr).value;
    }

    @Override
    public Iterator<T> iterator() {
        return new SinglyListIterator();
    }

    @Override
    public String toString() {
        var sb = new StringBuilder();
        if (isEmpty()) {
            sb.append("List is Empty!");
            return sb.toString();
        }

        ListNode<T> temp = head;
        while (temp.next != null) {
            sb.append(temp.value.toString())
                    .append(" -> ");
            temp = temp.next;
        }
        return sb.append(temp.value.toString())
                .toString();
    }

    private class SinglyListIterator implements Iterator<T> {

        private ListNode<T> nextNode;

        public SinglyListIterator() {
            this.nextNode = head;
        }

        @Override
        public boolean hasNext() {
            return this.nextNode != null;
        }

        @Override
        public T next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            var item = nextNode.value;
            nextNode = nextNode.next;
            return item;
        }
    }
}

Doubly Linked List

public class DoublyLinkedList<E> implements Iterable<E> {

    private ListNode<E> head;
    private int size;

    public boolean isEmpty() {
        return head == null;
    }

    public int size() {
        return size;
    }

    public void addFirst(E item) {
        if (isEmpty()) {
            head = new ListNode<>(null, item, null);
        } else {
            var curr = head;
            head = new ListNode<>(null, item, curr);
            curr.prev = head;
        }
        size++;
    }

    public E getFirst() {
        if (head == null) throw new NoSuchElementException();
        return head.item;
    }

    public void addLast(E item) {
        if (isEmpty()) {
            addFirst(item);
        } else {
            var curr = head;
            while (curr.next != null) {
                curr = curr.next;
            }
            curr.next = new ListNode<>(curr, item, null);
            size++;
        }
    }

    public E getLast() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        var curr = head;
        while (curr.next != null) {
            curr = curr.next;
        }
        return curr.item;
    }

    public E get(final int index) {
        checkPositionIndex(index);
        if (head == null) throw new IndexOutOfBoundsException();
        var curr = head;
        for (int i = 0; i < index; i++) {
            curr = curr.next;
        }
        if (curr == null) throw new IndexOutOfBoundsException();
        return curr.item;
    }

    public void add(int index, E item) {
        checkPositionIndex(index);
        if (index == size) {
            addLast(item);
        } else {
            link(item, node(index));
        }
    }

    public void insertAfter(E item, E newItem) {
        if (isEmpty()) {
            addFirst(newItem);
            return;
        }
        var curr = head;
        while (curr != null && !item.equals(curr.item)) {
            curr = curr.next;
        }
        if (curr != null) {
            curr.next = new ListNode<>(curr, newItem, curr.next);
            size++;
        }
    }

    public void insertBefore(E item, E newItem) {
        if (isEmpty()) {
            addFirst(newItem);
            return;
        }
        ListNode<E> prev = null;
        var curr = head;
        while (curr != null && !item.equals(curr.item)) {
            curr = curr.next;
        }
        if (curr != null) {
            var newNode = new ListNode<>(curr.prev, newItem, curr);
            curr.prev.next = newNode;
            curr.prev = newNode;
            size++;
        }
    }

    public void delete(E item) {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        if (item.equals(head.item)) {
            head = head.next;
            head.prev = null;
            size--;
            return;
        }
        var curr = head;
        while (curr != null && !item.equals(curr.item)) {
            curr = curr.next;
        }
        if (curr == null) {
            throw new NoSuchElementException("Element " + item.toString() + " not found");
        }
        if (curr.next != null) {
            curr.next.prev = curr.prev;
        }
        curr.prev.next = curr.next;
        size--;
    }

    private ListNode<E> node(int index) {
        var curr = head;
        for (int i = 0; i < index; i++) {
            curr = curr.next;
        }
        return curr;
    }

    private void link(E item, ListNode<E> succ) {
        var curr = head;
        while (curr.next != null && curr.next != succ) {
            curr = curr.next;
        }
        curr.next = new ListNode<>(curr, item, curr.next);
        size++;
    }

    private void checkPositionIndex(int index) {
        if (index < 0 || index > size)
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
    }

    @Override
    public Iterator<E> iterator() {
        return new DoublyListIterator();
    }

    private static class ListNode<T> {

        private T item;
        private ListNode<T> next;
        private ListNode<T> prev;

        public ListNode(ListNode<T> prev, T value, ListNode<T> next) {
            this.item = value;
            this.next = next;
            this.prev = prev;
        }

    }

    private class DoublyListIterator implements Iterator<E> {

        private ListNode<E> nextNode;

        public DoublyListIterator() {
            this.nextNode = head;
        }

        @Override
        public boolean hasNext() {
            return this.nextNode != null;
        }

        @Override
        public E next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            var item = nextNode.item;
            nextNode = nextNode.next;
            return item;
        }
    }
}