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.
-
draw(String s)
: takes aString
-
draw(int i)
: takes anint
-
draw(double d)
: takes adouble
-
draw(int i, double d)
: takes anint
and adouble
-
draw(double d, int i)
: takes adouble
and anint
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
-
the method name is the same, e.g.,
draw
-
the argument list differs in
-
the number of arguments
-
the types for the arguments
-
the order of the arguments
-
You cannot have two or more methods that have
-
the same name and
-
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.
And here is the code for the classes
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;
}
}
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;
}
}
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
What we want to check that
-
author1.first
is the same asauthor2.first
-
author1.last
is the same asauthor2.last
-
book1.title
is the same asbook2.title
-
author1.book
points tobook1
andauthor2.book
points tobook2
. The same instances (or objects) we already checked their titles. -
book1.author
points toauthor1
andbook2.author
points toauthor2
. The same instances (or objects) we already checked their first and last fields.
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;
}
}
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;
}
}
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
-
fully understand how each problem case maps to our runtime state
-
are able to map, using code, from our runtime state to the problem cases
-
deal with each case correctly per the problem specification
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
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
.
-
if the list is empty, then replace
null
with a newCell
that contains the newly added value and thenext
field ofCell
is null -
if the list is non-empty,
-
then create a new
Cell
that contains the newly added value and -
the
next
field points to the currenthead
-
update
head
to point to the newly createdCell
size
We need to count the number of elements
-
if the list is empty, then 0
-
if the list is not empty,
-
iterate over each
Cell
object and stop when wenext
points tonull
isEmpty
If head
points to null
then we are empty, else we are not.
contains
-
iterate over each
Cell
and check the value inside theCell
-
if the value is equal to the argument,
true
-
if the value is not equal tot he argument keep going
-
if we hit
null
in our iteration, returnfalse
getFirst
-
if we are empty, then error
-
else from
head
grab the value of the firstCell
and return it
getLast
-
if we are empty, then error
-
else iterate over each
Cell
until we find theCell
whosenext
field points tonull
, return the value inside thatCell
object.
addAtIndex
-
if the index provided is outside the bounds of the list, then error
-
if the index is 0 then getFirst
-
iterate down
index
elements, we will refer to theCell
we have iterated to ascurrent
and -
create a new
Cell
-
add the argument as the value of the new
Cell
-
set
next
for the newCell
to thecurrent.next
-
make
current.next
point to our newly createdCell
.
remove
-
if the list is empty, we are done
-
else iterate over each element and
-
if the current list element is equal to the argument then
-
grab the previous list elements
-
point previous list elements
next
field tocurrent.next
removeAtIndex
-
if the index provided is out of bounds, then error
-
else iterate over
index
element, we will refer to theCell
we have iterated to ascurrent
and -
grab the previous list element from
current
-
point previous list elements
next
field tocurrent.next
Design our classes
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 +
'}';
}
}
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
-
For each add operation we will increment our element counter.
-
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.
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.,
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
-
before the execution of a
public
method and -
immediately after the execution of a
public
method.
We are allowed to temporarily break the object invariant
-
during the execution of a
public
method; -
before the execution of a
private
method -
during the execution of a
private
method -
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.