Relevant Reading Material

Chapters from How to Design Classes (HtDC)

  • Chapters 24, 25, 26, 27, 28

Constructors and overloading

Recall our solution from last week for Student and Course and how we used the special reference value null in our constructors in order to create incomplete instances. We then use set methods to complete our instances and create our circular objects.

Student john = new Student("John", "Hart", 1234, null);
Course introToLogic = new Course(333, "Intro to Logic", null);

john.setCourse(introToLogic);
introToLogic.setStudent(john);

We will now use one of the Java language features called overloading to demonstrate another way to create such instances.

Overloading Or Ad-hoc polymorphism

Overloading (not be confused with overriding) allows us to create methods that share the same method name but differ in their signature. Overloading can be used for any method (constructor, static or non-static methods)

Let’s first see example of overloaded methods.

public class DataArtist {
    // ...
    public void draw(String s) {
    // ...
    }
    public void draw(int i) {
    // ...
    }
    public void draw(double d) {
    // ...
    }
    public void draw(int i, double d) {
    // ...
    }

    public void draw(double d, int i) {
    // ...
    }
}

Notice how we have five methods with the same name, draw. But each method has a different signature.

  1. draw(String s) : takes a String

  2. draw(int i) : takes an int

  3. draw(double d) : takes a double

  4. draw(int i, double d) : takes an int and a double

  5. draw(double d, int i) : takes a double and an int

These five methods are distinct and unique they happen to have the same method name.

You have been using overloaded methods from JUnit’s Assert class. The assertEquals methods are overloaded and they are static methods. JUnit Javadoc for Assert.

Calling an overloaded method is the same as calling any method. The Java compiler and runtime will call the appropriate method based on the signature that your method call matches, for example,

DataArtist dataArtist = new DataArtist();
dataArtist.draw("Hello");   // calls draw(String s)
dataArtist.draw(2);         // calls draw(int i)
dataArtist.draw(0.5d);      // calls draw(double d)
dataArtist.draw(3, 1.10d);  // calls draw(int i, double d)

Two (or more methods) are overloaded if

  1. the method name is the same, e.g., draw

  2. the argument list differs in

    1. the number of arguments

    2. the types for the arguments

    3. the order of the arguments

You cannot have two or more methods that have

  1. the same name and

  2. the same number of arguments with the same argument order and the same argument types.

A method’s return type is not considered when resolving overloaded method. You cannot declare two methods with the same signature even if they have a different return type.

Overloading and Subtypes

The Java compiler is responsible for figuring out which overloaded method will be called at runtime. Therefore, when calling an overloaded method whose arguments are reference type, the compiler will resolve (figure out which method to run) by using the compile-time type of the arguments!

Here is an example

public class Speaker {

  public String speak(String s){
    return "I say " + s;
  }

  public String speak(Object o) {
    return o.toString();
  }

  public String speak(Integer i){
    return "The number is " + i;
  }

  public static void main(String[] args){
    String s = "Hello";
    Integer n = new Integer(42);
    Speaker sp = new Speaker();

    System.out.println(sp.speak(s)); // I say Hello
    System.out.println(sp.speak(n)); // The number is 42

    Object so = (Object) s;
    Object no = (Object) n;
    Object spo = (Object) sp;

    System.out.println(sp.speak(so));  // Hello
    System.out.println(sp.speak(no));  // 42
    System.out.println(sp.speak(spo)); // Speaker@511d50c0
  }
}
Overloaded methods should be used sparingly.

Back to our Student, Course example

So let’s show an example with our constructors for Student and Course

We would like to have a constructor that our clients can use that only allows arguments that our clients can provide at the time of creation, i.e., hide the need for the null value.

public class Course {

  private Integer courseNumber;
  private String title;
  private Student student;


  private Course(Integer courseNumber, String title, Student student) { (1)
    this.courseNumber = courseNumber;
    this.title = title;
    this.student = student;
  }

  public Course(Integer courseNumber, String title) {  (2)
      this(courseNumber, title, null);  (3)
  }

  // elided code
}
1 Notice that our original constructor is now private so that the clients cannot use it.
2 This is a second constructor that only takes two arguments, a courseNumber and a title.
3 Calling a constructor from inside another constructor we use the syntax this().

Now the client can only call our constructor with two arguments. Our original constructor is now hidden from the clients. We reuse our original constructor but now we are in control of the initial value to use for the field student. We are using the value null at the moment, we will see another way later in the course.

Circular Data

Let’s look at a simplified representation of books and their authors. We will first restrict our design so that a book has one author and an author has one book.

studentcd2

And here is the code for the classes

Author.java
package simpleauthor;

import java.util.Objects;

public class Author {

  private String first;
  private String last;
  private Book book;

  public Author(String first, String last, Book book) {
    this.first = first;
    this.last = last;
    this.book = book;
  }


  @Override
  public boolean equals(Object other) {
    if (this == other) return true;
    if (other == null || getClass() != other.getClass()) return false;
    Author author = (Author) other;
    return Objects.equals(first, author.first)
        && Objects.equals(last, author.last)
        && Objects.equals(book, author.book);
  }

  @Override
  public int hashCode() {
    return Objects.hash(first, last, book);
  }

  /**
   * Getter for property 'first'.
   *
   * @return Value for property 'first'.
   */
  public String getFirst() {
    return first;
  }

  /**
   * Setter for property 'first'.
   *
   * @param first Value to set for property 'first'.
   */
  public void setFirst(String first) {
    this.first = first;
  }

  /**
   * Getter for property 'last'.
   *
   * @return Value for property 'last'.
   */
  public String getLast() {
    return last;
  }

  /**
   * Setter for property 'last'.
   *
   * @param last Value to set for property 'last'.
   */
  public void setLast(String last) {
    this.last = last;
  }

  /**
   * Getter for property 'book'.
   *
   * @return Value for property 'book'.
   */
  public Book getBook() {
    return book;
  }

  /**
   * Setter for property 'book'.
   *
   * @param book Value to set for property 'book'.
   */
  public void setBook(Book book) {
    this.book = book;
  }
}
Book.java
package simpleauthor;

import java.util.Objects;

public class Book {

  private String title;
  private Author author;

  public Book(String title, Author author) {
    this.title = title;
    this.author = author;
  }

  @Override
  public boolean equals(Object other) {
    if (this == other) return true;
    if (other == null || getClass() != other.getClass()) return false;
    Book book = (Book) other;
    return Objects.equals(title, book.title)
        && Objects.equals(author, book.author);
  }

  @Override
  public int hashCode() {
    return Objects.hash(title, author);
  }

  /**
   * Getter for property 'title'.
   *
   * @return Value for property 'title'.
   */
  public String getTitle() {

    return title;
  }

  /**
   * Setter for property 'title'.
   *
   * @param title Value to set for property 'title'.
   */
  public void setTitle(String title) {
    this.title = title;
  }

  /**
   * Getter for property 'author'.
   *
   * @return Value for property 'author'.
   */
  public Author getAuthor() {
    return author;
  }

  /**
   * Setter for property 'author'.
   *
   * @param author Value to set for property 'author'.
   */
  public void setAuthor(Author author) {
    this.author = author;
  }
}
AuthorTest.java
package simpleauthor;

import org.junit.Assert;
import org.junit.Test;

public class AuthorTest {

  private Author a1;
  private Author a2;
  private Author a3;
  private Book b1;
  private Book b2;
  private Book b3;


  @org.junit.Before
  public void setUp() throws Exception {
    this.a1 = new Author("A", "B", null);
    this.a2 = new Author("A", "B", null);
    this.a3 = new Author("X", "Y", null);

    this.b1 = new Book("B1", a1);
    this.a1.setBook(b1);

    this.b2 = new Book("B1", a2);
    this.a2.setBook(b2);

    this.b3 = new Book("Q", a3);
    this.a3.setBook(b3);
  }

  @Test
  public void testEquals() throws Exception {
    Assert.assertTrue(this.a1.equals(a2));
  }
}

Running our test results in a StackOverflowException.

Walk over stack creation on the board.

Break the cycle

Object instance for our Test
Figure 1. Object instance for our Test

What we want to check that

  1. author1.first is the same as author2.first

  2. author1.last is the same as author2.last

  3. book1.title is the same as book2.title

  4. author1.book points to book1 and author2.book points to book2. The same instances (or objects) we already checked their titles.

  5. book1.author points to author1 and book2.author points to author2. The same instances (or objects) we already checked their first and last fields.

Author.java
package simpleauthornoloop;

import java.util.Objects;

public class Author {

  private String first;
  private String last;
  private Book book;

  public Author(String first, String last, Book book) {
    this.first = first;
    this.last = last;
    this.book = book;
  }

  @Override
  public boolean equals(Object other) {
    if (this == other) return true;
    if (other == null || getClass() != other.getClass()) return false;
    Author author = (Author) other;

    return Objects.equals(first, author.first)
        && Objects.equals(last, author.last)
        && book.equalsBook(this, author.book, author);
  }

  public boolean equalsAuthor(Book thisBook, Author other, Book checkedBook){
    if (this == other) return true;
    if (other == null) return false;
    if (this.book != thisBook) return false;
    if (other.book != checkedBook) return false;
    return Objects.equals(first, other.first)
        && Objects.equals(last, other.last);
  }


  @Override
  public int hashCode() {
    return Objects.hash(first, last, book);
  }

  /**
   * Getter for property 'first'.
   *
   * @return Value for property 'first'.
   */
  public String getFirst() {
    return first;
  }

  /**
   * Setter for property 'first'.
   *
   * @param first Value to set for property 'first'.
   */
  public void setFirst(String first) {
    this.first = first;
  }

  /**
   * Getter for property 'last'.
   *
   * @return Value for property 'last'.
   */
  public String getLast() {
    return last;
  }

  /**
   * Setter for property 'last'.
   *
   * @param last Value to set for property 'last'.
   */
  public void setLast(String last) {
    this.last = last;
  }

  /**
   * Getter for property 'book'.
   *
   * @return Value for property 'book'.
   */
  public Book getBook() {
    return book;
  }

  /**
   * Setter for property 'book'.
   *
   * @param book Value to set for property 'book'.
   */
  public void setBook(Book book) {
    this.book = book;
  }
}
Book.java
package simpleauthornoloop;

import java.util.Objects;

public class Book {

  private String title;
  private Author author;

  public Book(String title, Author author) {
    this.title = title;
    this.author = author;
  }

  @Override
  public boolean equals(Object other) {
    if (this == other) return true;
    if (other == null || getClass() != other.getClass()) return false;
    Book book = (Book) other;
    return Objects.equals(title, book.title)
        && author.equalsAuthor(this, book.author, book);
  }

  public boolean equalsBook(Author thisAuthor, Book other, Author checkedAuthor) {
    if (this == other) return true;
    if (other == null) return false;
    if (this.author != thisAuthor) return false;
    if (other.author != checkedAuthor) return false;
    return Objects.equals(title, other.title);
  }

  @Override
  public int hashCode() {
    return Objects.hash(title, author);
  }

  /**
   * Getter for property 'title'.
   *
   * @return Value for property 'title'.
   */
  public String getTitle() {

    return title;
  }

  /**
   * Setter for property 'title'.
   *
   * @param title Value to set for property 'title'.
   */
  public void setTitle(String title) {
    this.title = title;
  }

  /**
   * Getter for property 'author'.
   *
   * @return Value for property 'author'.
   */
  public Author getAuthor() {
    return author;
  }

  /**
   * Setter for property 'author'.
   *
   * @param author Value to set for property 'author'.
   */
  public void setAuthor(Author author) {
    this.author = author;
  }
}
AuthorTest.java
package simpleauthornoloop;

import org.junit.Assert;
import org.junit.Test;

public class AuthorTest {

  private Author a1;
  private Author a2;
  private Author a3;
  private Book b1;
  private Book b2;
  private Book b3;


  @org.junit.Before
  public void setUp() throws Exception {
    this.a1 = new Author("A", "B", null);
    this.a2 = new Author("A", "B", null);
    this.a3 = new Author("X", "Y", null);

    this.b1 = new Book("B1", a1);
    this.a1.setBook(b1);

    this.b2 = new Book("B1", a2);
    this.a2.setBook(b2);

    this.b3 = new Book("Q", a3);
    this.a3.setBook(b3);
  }

  @Test
  public void testEquals() throws Exception {
    Assert.assertTrue(this.a1.equals(a2));
  }
}

Singly Linked Lists

Similar to our Cons lists but we are going to use mutation instead. The main take away here is that with mutation (or imperative style) we are mapping the different cases from our problem domain to different configurations of our object’s state. It is crucial that we

  1. fully understand how each problem case maps to our runtime state

  2. are able to map, using code, from our runtime state to the problem cases

  3. deal with each case correctly per the problem specification

List.java
package sllist;

/**
 * Linked List interface.
 */
public interface List {

  /**
   * Create an empty list.
   *
   * <pre>
   *   pre : true
   *   post: result.isEmpty() == true
   * </pre>
   *
   * @return  the empty list
   */
  static List create(){
    return new SLList();
  }

  /**
   * Add {@code element} as the first item in the list.
   *
   * <pre>
   *  pre : element != null
   *  post: old.size() + 1 == result.size() &&
   *        result.contains(element) &&
   *        result.getFirst().equals(element)
   * </pre>
   * @param element new element to be added to beggining (index 0) of the list.
   */
  void add(Integer element);

  /**
   * Return the number of elements currently in the list.
   *
   * <pre>
   *  pre : true
   *  post: |this|
   * </pre>
   * @return total number of elements in the list
   */
  Integer size();

  /**
   * Check if the list is empty.
   *
   * <pre>
   *   pre : true
   *   post: true <=> this.size() == 0
   * </pre>
   * @return true if this is an empty list, false otherwise.
   */
  Boolean isEmpty();

  /**
   * Check if {@code element} is already in the list.
   *
   * <pre>
   *   pre : element != null
   *   post: true <=> this.getFirst().equals(element) ||
   *                  this.remove(this.getFirst());
   *                  this.contains(element)
   * </pre>
   *
   * @param element the element we want to check
   * @return true if element is in the list, false otherwise
   */
  Boolean contains(Integer element);


  /**
   * Getter for property 'first'.
   *
   * <pre>
   *   pre : !this.isEmpty()
   * </pre>
   * @return Value for property 'first'.
   */
  Integer getFirst();

  /**
   * Getter for property 'last'.
   *
   *
   * <pre>
   *   pre : !this.isEmpty()
   * </pre>
   * @return Value for property 'last'.
   */
  Integer getLast();

  /**
   * Add {@code element} at location {@code index} in the list.
   *
   * <pre>
   *   pre: size() >= index >=0
   * </pre>
   *
   * @param index location to which the element will be added in this list.
   * @param element the new element to add.
   */
  void addAtIndex(Integer index, Integer element);

  /**
   * Remove {@code element} from the list. If the element is not found, then
   * the list remains unchanged.
   *
   * @param element value to be deleted from the list
   */
  void remove(Integer element);

  /**
   * Remove the element found at {@code index} in the list.
   *
   * Where:
   *
   * <pre>
   *   size() > index >= 0
   * </pre>
   * @param index location in the list to be removed.
   */
  void removeAtIndex(Integer index);

}
Follow the Design Recipe!

Let’s make examples and walk through them

Empty singly linked list.
Figure 2. Empty singly linked list.
One element singly linked list.
Figure 3. One element singly linked list.
More than one element singly liked list.
Figure 4. More than one element singly liked list.

Let’s walk through want needs to happen for each operation.

Perform the steps on the whiteboard using the object diagrams from our examples

add

So we are adding the new element at the start of the list on the side of head.

  1. if the list is empty, then replace null with a new Cell that contains the newly added value and the next field of Cell is null

  2. if the list is non-empty,

  3. then create a new Cell that contains the newly added value and

  4. the next field points to the current head

  5. update head to point to the newly created Cell

size

We need to count the number of elements

  1. if the list is empty, then 0

  2. if the list is not empty,

  3. iterate over each Cell object and stop when we next points to null

isEmpty

If head points to null then we are empty, else we are not.

contains

  1. iterate over each Cell and check the value inside the Cell

  2. if the value is equal to the argument, true

  3. if the value is not equal tot he argument keep going

  4. if we hit null in our iteration, return false

getFirst

  1. if we are empty, then error

  2. else from head grab the value of the first Cell and return it

getLast

  1. if we are empty, then error

  2. else iterate over each Cell until we find the Cell whose next field points to null, return the value inside that Cell object.

addAtIndex

  1. if the index provided is outside the bounds of the list, then error

  2. if the index is 0 then getFirst

  3. iterate down index elements, we will refer to the Cell we have iterated to as current and

  4. create a new Cell

  5. add the argument as the value of the new Cell

  6. set next for the new Cell to the current.next

  7. make current.next point to our newly created Cell.

remove

  1. if the list is empty, we are done

  2. else iterate over each element and

  3. if the current list element is equal to the argument then

  4. grab the previous list elements

  5. point previous list elements next field to current.next

removeAtIndex

  1. if the index provided is out of bounds, then error

  2. else iterate over index element, we will refer to the Cell we have iterated to as current and

  3. grab the previous list element from current

  4. point previous list elements next field to current.next

Design our classes

Class diagram for Singly Linked List.
Figure 5. Class diagram for Singly Linked List.
SLList.java
package sllist;


import java.util.Objects;

/**
 * Created by therapon on 6/12/16.
 */
public class SLList implements List {

  private Cell head;

  public SLList() {
    this.head = null;
  }

  @Override
  public void add(Integer element) {
    if (isEmpty()) {
      setHead(new Cell(element, null));
    } else {
      setHead(new Cell(element, getHead()));
    }
  }

  @Override
  public Integer size() {
    Integer count = 0;
    Cell current = getHead();

    while (current != null) {
      count++;
      current = current.getNext();
    }
    return count;
  }

  @Override
  public Boolean isEmpty() {
    return head == null;
  }

  @Override
  public Boolean contains(Integer element) {
    Cell current = getHead();
    while (current != null) {
      if (current.getVal().equals(element)) {
        return true;
      } else {
        current = current.getNext();
      }
    }
    return false;
  }

  @Override
  public Integer getFirst() {
    if (isEmpty()) {
      throw new ListIllegalOperationException("Called getFirst on empty list");
    }
    return getHead().getVal();
  }

  @Override
  public Integer getLast() {
    if (isEmpty()) {
      throw new ListIllegalOperationException("Attempted to get last element from an empty list");
    } else {
      Cell current = getHead();
      while (current.getNext() != null) {
        current = current.getNext();
      }
      return current.getVal();
    }
  }

  @Override
  public void addAtIndex(Integer index, Integer element) {
    if (index < 0 || index > this.size()) {
      throw new ListIllegalOperationException("Invalid index for list");
    }

    if (index == 0) {
      add(element);
    } else {

      Cell current = getHead();
      while (index > 1) {
        current = current.getNext();
        index = index - 1;
      }
      Cell newEntry = new Cell(element, current.getNext());
      current.setNext(newEntry);
    }
  }


  @Override
  public void remove(Integer element) {
    if (isEmpty()) return;
    else if (getHead().getVal().equals(element)) {
      setHead(getHead().getNext());
    } else {
      removeHelper(getHead(), getHead().getNext(), element);
    }
  }

  private void removeHelper(Cell previous, Cell current, Integer element) {
    if (current == null) return;
    if (current.getVal().equals(element)) {
      previous.setNext(current.getNext());
    } else {
      removeHelper(current, current.getNext(), element);
    }
  }

  @Override
  public void removeAtIndex(Integer index) {
    if (index < 0 || index > this.size() - 1) {
      throw new ListIllegalOperationException("Invalid index for list");
    }
    if (isEmpty()) return;
    else if (index == 0) {
      setHead(getHead().getNext());
    } else {
      removeAtIndexHelper(getHead(), getHead().getNext(), index - 1);
    }
  }

  private void removeAtIndexHelper(Cell previous, Cell current, Integer index) {
    while (index != 0) {
      index = index - 1;
      previous = current;
      current = current.getNext();
    }
    previous.setNext(current.getNext());
  }

  /**
   * Getter for property 'head'.
   *
   * @return Value for property 'head'.
   */

  public Cell getHead() {
    return head;
  }

  /**
   * Setter for property 'head'.
   *
   * @param head Value to set for property 'head'.
   */
  public void setHead(Cell head) {
    this.head = head;
  }

  @Override
  public boolean equals(Object other) {
    if (this == other) return true;
    if (other == null || getClass() != other.getClass()) return false;
    SLList slList = (SLList) other;
    return Objects.equals(head, slList.head);
  }

  @Override
  public int hashCode() {
    return Objects.hash(head);
  }

  @Override
  public String toString() {
    return "SLList{" +
        "head=" + head +
        '}';
  }


}
Cell.java
package sllist;

import java.util.Objects;

/**
 * Created by therapon on 6/12/16.
 */
class Cell {


  private Integer val;
  private Cell next;

  public Cell(){}
  public Cell(Integer val, Cell next) {
    this.val = val;
    this.next = next;
  }



  /**
   * Getter for property 'next'.
   *
   * @return Value for property 'next'.
   */
  public Cell getNext() {
    return next;
  }

  /**
   * Setter for property 'next'.
   *
   * @param next Value to set for property 'next'.
   */
  public void setNext(Cell next) {
    this.next = next;
  }


  /**
   * Getter for property 'val'.
   *
   * @return Value for property 'val'.
   */
  public Integer getVal() {
    return val;
  }

  /**
   * Setter for property 'val'.
   *
   * @param val Value to set for property 'val'.
   */
  public void setVal(Integer val) {
    this.val = val;
  }


  @Override
  public boolean equals(Object other) {
    if (this == other) return true;
    if (other == null || getClass() != other.getClass()) return false;
    Cell cell = (Cell) other;
    return Objects.equals(val, cell.val)
        && Objects.equals(next, cell.next);
  }

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

  @Override
  public String toString() {
    return "Cell{" +
        "val=" + val +
        ", next=" + next +
        '}';
  }
}

Caching

Now that we know how to update fields, there is another way to implement size. We could keep a private field that acts as our element counter. Our code will initialize this field to 0 on creation of our list and then

  1. For each add operation we will increment our element counter.

  2. For each remove operation we will decrement our element counter.

In this implementation the method size will simply return the value of our element counter field.

SLList with element counter
package sllistcount;


import java.util.Objects;

/**
 * List implementation as a singly linked list.
 * <p>
 * Object invariant:
 * <pre>
 *   elementCount = |this|
 *
 * </pre>
 */
public class SLList implements List {

  private Cell head;
  private Integer elementCount;

  public SLList() {
    this.head = null;
    this.elementCount = 0;
  }

  @Override
  public void add(Integer element) {
    if (isEmpty()) {
      setHead(new Cell(element, null));
    } else {
      setHead(new Cell(element, getHead()));
    }
    elementCount = elementCount + 1;
  }

  @Override
  public Integer size() {
    return elementCount;
  }

  @Override
  public Boolean isEmpty() {
    return head == null;
  }

  @Override
  public Boolean contains(Integer element) {
    Cell current = getHead();
    while (current != null) {
      if (current.getVal().equals(element)) {
        return true;
      } else {
        current = current.getNext();
      }
    }
    return false;
  }

  @Override
  public Integer getFirst() {
    if (isEmpty()) {
      throw new ListIllegalOperationException("Called getFirst on empty list");
    }
    return getHead().getVal();
  }

  @Override
  public Integer getLast() {
    if (isEmpty()) {
      throw new ListIllegalOperationException("Attempted to get last element from an empty list");
    } else {
      Cell current = getHead();
      while (current.getNext() != null) {
        current = current.getNext();
      }
      return current.getVal();
    }
  }

  @Override
  public void addAtIndex(Integer index, Integer element) {
    if (index < 0 || index > this.size()) {
      throw new ListIllegalOperationException("Invalid index for list");
    }

    if (index == 0) {
      add(element);

    } else {
      Cell current = getHead();
      while (index > 1) {
        current = current.getNext();
        index = index - 1;
      }
      Cell newEntry = new Cell(element, current.getNext());
      current.setNext(newEntry);
    }
    elementCount = elementCount + 1;
  }


  @Override
  public void remove(Integer element) {
    if (isEmpty()) return;
    else if (getHead().getVal().equals(element)) {
      setHead(getHead().getNext());
      elementCount = elementCount - 1;
    } else {
      removeHelper(getHead(), getHead().getNext(), element);
    }
  }

  private void removeHelper(Cell previous, Cell current, Integer element) {
    while (current != null) {
      if (current.getVal().equals(element)) {
        previous.setNext(current.getNext());
        elementCount = elementCount - 1;
      }
      previous = current;
      current = current.getNext();
    }
  }

  @Override
  public void removeAtIndex(Integer index) {
    if (index < 0 || index > this.size() - 1) {
      throw new ListIllegalOperationException("Invalid index for list");
    }
    if (isEmpty()) return;
    else if (index == 0) {
      setHead(getHead().getNext());
      elementCount = elementCount - 1;
    } else {
      removeAtIndexHelper(getHead(), getHead().getNext(), index - 1);
    }
  }

  private void removeAtIndexHelper(Cell previous, Cell current, Integer index) {
    while (index != 0) {
      index = index - 1;
      previous = current;
      current = current.getNext();
    }
    previous.setNext(current.getNext());
    elementCount = elementCount - 1;
  }

  /**
   * Getter for property 'head'.
   *
   * @return Value for property 'head'.
   */

  public Cell getHead() {
    return head;
  }

  /**
   * Setter for property 'head'.
   *
   * @param head Value to set for property 'head'.
   */
  public void setHead(Cell head) {
    this.head = head;
  }

  @Override
  public boolean equals(Object other) {
    if (this == other) return true;
    if (other == null || getClass() != other.getClass()) return false;
    SLList slList = (SLList) other;
    return Objects.equals(head, slList.head);
  }

  @Override
  public int hashCode() {
    return Objects.hash(head);
  }

  @Override
  public String toString() {
    return "SLList{" +
        "head=" + head +
        '}';
  }


}

Object invariants

Our implementation of size that relies on elementCounter imposes a condition on our objects. The condition is that at any given point in time during execution and for each object of type SLList the value stored in the field elementCount must be the number of elements currently in that list, i.e.,

\[ elementCount = |this|\]

These kinds of conditions that need to hold for each object and for the lifespan of the object are called object invariants. More specifically, our object invariants need to hold true

  1. before the execution of a public method and

  2. immediately after the execution of a public method.

We are allowed to temporarily break the object invariant

  1. during the execution of a public method;

  2. before the execution of a private method

  3. during the execution of a private method

  4. immediately after the execution of a private method

These are essentially the windows of time that our code will update our object’s state in order to make our object invariant hold true.