Basic Singly Linked List Implementation Purpose

I wanted to try and implement a very basic Singly Linked List as a data structure exercise.

Implementation Discussion

I know that the Java LinkedList API is quite
extensive. However, in this basic implementation, I just wanted to implement the addAtIndex, removeAtIndex, and
get methods (with the add and remove methods delegating to the addAtIndex and removeAtIndex methods).

Things that I could improve


For the addAtIndex and removeAtIndex methods I special-case when the index is 0 - is there a more elegant solution?
Do the exceptions make sense? 


For the remove method should I check to see if the list is empty and then throw an
IllegalStateException before calling the removeAtIndex method? Same thing for the get method?
Should I replace the IllegalArgumentExceptions with IndexOutOfBoundExceptions? 



Implementation



import java.util.Objects;

public class SinglyLinkedList&lt;T&gt; {

    public static class Node&lt;E&gt; {
        private final E data;
        private Node&lt;E&gt; next;

        public Node(E data, Node&lt;E&gt; next) {
            this.data = data;
            this.next = next;
        }

        public E getData() {
            return this.data;
        }

        public Node&lt;E&gt; getNext() {
            return this.next;
        }

        public void setNext(Node&lt;E&gt; next) {
            this.next = next;
        }

        @Override
        public int hashCode() {
            return Objects.hash(this.data, this.next);
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == this) {
                return true;
            }

            if (!(obj instanceof Node)) {
                return false;
            }

            Node node = (Node) obj;

            return Objects.equals(this.data, node.data) &amp;&amp;
                    Objects.equals(this.next, node.next);
        }
    }

    private Node&lt;T&gt; head = null;
    private int size = 0;

    public SinglyLinkedList() {
    }

    public int getSize() {
        return this.size;
    }

    public boolean isEmpty() {
        return this.getSize() == 0;
    }

    public Node&lt;T&gt; getHead() {
        return this.head;
    }

    public void add(T data) {
        this.addAtIndex(data, this.size);
    }

    public void remove() {
        this.removeAtIndex(this.size - 1);
    }

    public void addAtIndex(T data, int index) {
        if (index &gt; this.size || index &lt; 0) {
            throw new IllegalArgumentException("Invalid index value");
        }

        Node&lt;T&gt; currentNode = this.head;

        if (index == 0) {
            this.head = new Node&lt;&gt;(data, currentNode);
        } else {
            for (int i = 0; i &lt; index - 1; i++) {
                currentNode = currentNode.getNext();
            }

            Node&lt;T&gt; nextNode = new Node&lt;&gt;(data, currentNode.getNext());
            currentNode.setNext(nextNode);
        }

        this.size++;
    }

    public void removeAtIndex(int index) {
        if (index &lt; 0 || index &gt;= this.size) {
            throw new IllegalArgumentException("Invalid index value");
        }

        Node&lt;T&gt; currentNode = this.head;

        if (index == 0) {
            this.head = currentNode.getNext();
        } else {
            for (int i = 0; i &lt; index - 1; i++) {
                currentNode = currentNode.getNext();
            }

            currentNode.setNext(currentNode.getNext().getNext());
        }

        this.size--;
    }

    public T get(int index) {
        if (index &lt; 0 || index &gt;= this.size) {
            throw new IllegalArgumentException("Invalid index value");
        }

        Node&lt;T&gt; currentNode = this.head;

        int count = 0;
        while (count &lt; index) {
            currentNode = currentNode.getNext();
            count++;
        }

        return currentNode.getData();
    }

}
I think it's looking pretty good so far!

A few things,

I would make the Node class private. This is an implementation detail and nobody using your list should need to know or worry about it. I would also remove any public facing methods that return a Node over a T. If you want them internally you can make them private. (The built in Java LinkedList uses a private Node class but we don't need to worry about it)

You could remove a lot of boiler plate getters and setters by making the Node class private. You can access the fields directly within your SinglyLinkedList class.

When someone passes in a bad index, I would prefer and IndexOutOfBoundsException over IllegalArgumentException. (this is what java.util.Lists do afterall)

As well as this, I'd also like to see a more descriptive exception message. Maybe say which index was used, and what the current size is of the list.

You've overridden equals and hashCode in your node class which is usually a good thing to do in general, but here you don't really need it. However you may like to keep it if you end up adding additional functionality that may rely on it.

I think special casing index 0 is perfectly fine. 2 distinct things happen depending on it being the first element or any subsequent one.

A nice toString method would also be good here, you could just use a StringBuilder or StringJoiner to do this easily.