Relevant Reading Material
Chapters from How to Design Classes (HtDC)
-
Chapters 20, Intermezzo 3, 23
Behavioural Subtypes
In our designs we have used the property that a subclass can be used in the place of the super class.
With ADTs we also need to ensure that the behaviour of our subclasses is compatible with the behaviour of our superclass.
Types as Sets
A Java class defines two aspects of a type
-
data
-
behaviour
With ADTs we are interested in behaviour. Informally, and for the purpose of this discussion, we will map Java Classes to a set of values. A value is in the set of a type if and only if
-
the value is created using the class' constructor or
-
a subclass' constructor.
For example, Java’s Integer
class can hold any integer value in the range (231 - 1, -231), so the set of number in the range (231 - 1, -231) defines the complete set of values that an instance of Integer
can take.
We can use logical formulae (boolean expressions) to
-
describe the conditions that we expect to hold true before calling a method. Conditions can refer
-
the state of our object
-
impose restrictions to the values passed as arguments
-
-
describe the conditions that we expect to hold true as soon as the method returns. Conditions can refer
-
to the return value
-
the relations of the return value and the original values passed as arguments
-
the relations between the state of the object before it was called and the state of the object after returning a result
-
These conditions are akin to the WHERE
clauses that we wrote in CS5001.
These conditions are called software contracts, or, contracts for short. They are akin to legal contracts that specify an agreement between 2 parties. In software these two parties are
-
the client, code that calls a method
-
the implementation, the body of the method being called.
Software contracts are further subdivided into
-
The formulae that describes the conditions we expect to hold before calling a method is called a pre-condition.
-
The formulae that describes the conditions we expect to hold immediately after the method returns is called a post-condition.
Lets update our ADT specification with pre- and post-conditions for each operation. We will use a mixture of logical expression and Java syntax to write our pre- and post-conditions. We will also use the special names
-
result
to refer to the value returned by an operation -
old
to refer to the state of our ADT at the time the operation was called -
this
to refer to our ADT when there is no difference in the state of the ADT before and after an operation is called
Operation | Specification | Contracts |
---|---|---|
|
|
|
|
(()).isEmpty() = true ((1,2,...)).isEmpty() = false |
result -> this.equals( (()) ) AND !result -> !this.equals( (()) ) |
|
(()).add(1) = ((1)) ((1,2,...)).add(0) = ((0,1,2,...)) |
result.size() == old.size() + 1 AND result.contains(ele) AND for (i = result.size() ; i >= 2; i--) { result.elementAt(i) == old.elementAt(i - 1) } |
|
(()).contains(1) = false ((1,2,...)).contains(1) = true ((1,2,...)).contains(x) = ((2,...)).contains(x) |
result && this.size() > 0 && exists x ( 1 <= x <= this.size() && this.elementAt(x).equals(ele)) OR !result && ( this.size() == 0 OR (this.size() > 0 && forall x ( 1 <= x <= this.size(): ! this.elementAt(x).equals(ele)))) |
|
(()).size() = 0 ((1,2,...)).size() = 1 + ((2,...)).size() |
result == |this| |
|
(()).elementAt(n) = ERROR ((1,2,...)).elementAt(1) = 1 ((1,2,...)).elementAt(n) = ((2,...)).elementAt(n - 1) |
|
|
(()).tail() = ERROR ((1,2,...)).tail() = ((2,...)) |
result.size() + 1 == old.size() AND result.add(old.elementAt(0)).equals(old) |
Thinking through an example
List
and Student
including packages. Package local classes are denoted in gray.Our client Student
is happily using our List
implementation.
There is however discussion to update our implementation. So two separate teams went out to design a new implementation of List
.
For each method in List
interface we are going to provide a new implementation. Let’s iterate the options that we have with regards to the
set of values allowed as input and given as output for each operation
-
keep the sets the same; no alterations to the contracts of each operation, the client code should work as expected
-
increase the input sets; accept more inputs but keep the output set the same. This may allow our implementation to accommodate for more, possibly, new clients.
-
decrease the input sets; accept less inputs but keep the output set the same. This may allow our implementation to accommodate for more new clients
-
increase the output sets; return more outputs but keep the inputs the same. This may allow our implementation to accommodate for more new clients. . decrease the output sets; return less outputs but keep the inputs the same.
Draw sets on the board and discuss. |
Liskov’s Substitutability Principle (LSP)
Liskov’s substitution principle (LSP) states that for two types T
and S
such that S
is a subtype of T
, we should
be able to take existing client code that used T
, and replacement with objects of type S
without altering/breaking
the clients contracts (pre- and post-conditions).
So if we have
The logical implications that we need to ensure hold true are
-
A.m
pre ⇒B.m
pre and -
B.m
post ⇒A.m
post
The implications extend to longer inheritance chains.
Consider the implications that need to hold at D
-
A.m
pre ⇒B.m
pre ⇒C.m
pre ⇒D.m
pre and -
D.m
post ⇒C.m
post ⇒B.m
post ⇒A.m
post
Arrays
Arrays in Java provide a mechanism to store a fixed size collection of values. The syntax of arrays in Java can be a little confusing. Even though arrays in Java are reference types, the class is hidden from you and is part of the Java language.
Array Type
To define a variable that has an array type we need to use []
and the type of the element(s) to be stored in the array.
Integer[] grades; // an array that stores Integer objects
String[] names; // an array that stores String objects
Array initialization
We can initialize an array in place
Integer[] grades = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
Or we allocate the space needed and then using the index operator set each location in the array
Integer[] grades = new Integer[10]; // observer the use of `new` here even though there is no class
grades[0] = 0; // index starts at 0 not 1
grades[1] = 1;
grades[2] = 2;
grades[3] = 3;
grades[4] = 4;
grades[5] = 5;
grades[6] = 6;
grades[7] = 7;
grades[8] = 8;
grades[9] = 9;
Array access
Using the index operator.
What happens when we index something out of bounds?
Array length
Array objects provide methods and fields to their clients. length
is the field that stores the max capacity of the array
Integer[] grades = new Integer[10];
System.out.println("Array size is: " + grades.length); // prints 10 even though there are no elements
System.out.println(grades[0]); // What is the value at index 0?
Outputs:
Array size is: 10
null
Looping over arrays
The typical way to iterate over the array? for
loop!
Integer[] grades = new Integer[10]; // observer the use of `new` here even though there is no class
grades[0] = 0; // index starts at 0 not 1
grades[1] = 10;
grades[2] = 20;
grades[3] = 30;
grades[4] = 40;
grades[5] = 50;
grades[6] = 60;
grades[7] = 70;
grades[8] = 80;
grades[9] = 90;
for(int i = 0; i < grades.length; i++){
System.out.println("At index: " + i + " value is: " + grades[i]);
}
Outputs
At index: 0 value is: 0
At index: 1 value is: 10
At index: 2 value is: 20
At index: 3 value is: 30
At index: 4 value is: 40
At index: 5 value is: 50
At index: 6 value is: 60
At index: 7 value is: 70
At index: 8 value is: 80
At index: 9 value is: 90
If you are not interested on the index or you are not performing computation that jumps around based on the index, there is a better (preferred) way to do this
for (Integer element : grades) {
System.out.println("value is: " + element);
}
Outputs
value is: 0
value is: 10
value is: 20
value is: 30
value is: 40
value is: 50
value is: 60
value is: 70
value is: 80
value is: 90
We can turn any for
into a while
loop
int i = 0;
while (i < grades.length) {
System.out.println("At index: " + i + " value is: " + grades[i]);
i++; // equivalent to `i = i + 1;`
}
Looping, not just for arrays
Consider the following List
ADT
public interface List {
public static List createEmpty(){
// ...
}
public Integer getFirst();
public Boolean isEmpty();
public List getRest();
public List add(Integer element);
}
We could loop over the list using a while
loop
List grades = List.createEmpty().add(10).add(20).add(30).add(40);
while(!grades.isEmpty()) {
System.out.println("Element = " + grades.getFirst());
grades = grades.getRest(); // reset `grades` to the tail of the list
}
// lets do it in a for as well
for(; !grades.isEmpty();) {
System.out.println("Element = " + grades.getFirst());
grades = grades.getRest();
}
Observe that the code for iterating over our List
here has a pattern. This pattern can be used to re-write (or refactor) a
recursive method definition into a while
(or for
) loop.
We need a variable to store the value(s) we are going to recur on. In our example that value was grades
.
The test in our while
(or for loop
) is the base case, it is the condition that will tell us we are done with our recursion.
The body of our while
is the implementation of our recursive step(s), what was in our Cons
class and the last line in our
body for while
re-assigns are variable to the next value in our recursion. This is the getRest()
call in our Cons
method implementation
that recurs on the rest of our list.
Multidimensional arrays
We can have a matrix (2-dimensional) arrays.
Integer[][] matrix = new Integer[5][10]; // 5 arrays, each array with 10 elements
We can access each array and its elements using the array index operation e.g.,
matrix[0]; // first array of 10 elements
matrix[0][2]; // first array of 10 elements, then pick the 3rd element
We can have more than 2 dimensions.
Integer[][][] t = new Integer[5][10][3]; // can you draw the structure?
The main
method in Java
So there is a special static method called main
that Java and your Operating System uses as the default entry point to your code.
class Main {
public static void main(String[] args) {
// start executing my program here!
}
}
The argument to main
is an array of String
s that will hold the text passed to the JVM as extra arguments.
We will talk more about this later in the semester.
Java Primitive Types
Up to this point we were using reference types in our Java programs. There are also primitive types as well. These is a second category of types and values that the Java language lets us use.
-
int
,double
,float
,byte
,short
,long
- deal with numbers at different precision and range -
boolean
- primitive type fortrue
andfalse
-
char
- primitive type for a unicode character
The primitives have a corresponding reference type with the same name, but the first name is Capital! Java 8, can automatically take in an int
and turn it into an Integer
, this is called boxing. Similarly Java can take a reference type and turn it into a primitive, i.e., Integer
to int
, this is called unboxing. These two operations boxing and unboxing are collectively called autoboxing.
public interface Adder{
void add(int x);
void add(Integer x);
}
As a rule of thumb you should use primitive types for data that you would like to capture to which you do not want to add behaviour/operations (methods). Typically you would like to "wrap" your primitive data with a reference type that captures the information in your problem domain.
class Person {
private Name name;
private Age age;
// methods elided
}
class Age {
private int age;
// methods elided
}
Circular Data
Consider a school with a registration system that allows a student to register for a course. To make things a little simpler, lets assume for now that a course can have only one student. We will relax this restriction later on.
The information that we would like to capture for a course and a student are as follows.
For a course we would like to capture
-
a course number
-
a course title
-
the student taking the course
For a student we would like to capture
-
first name
-
last name
-
student id
-
the course the student is taking
Here is a UML Class Diagram for our program
We can create these classes with constructors and getter methods just like we have been doing thus far in the course.
Creating an instance of Student
and Course
however is not possible.
Student john = new Student("John", "Hart", 1234, new Course(333, "Intro to Logic", ???));
What we would like to say at the location marked ???
is john
, the student we are creating. But we cannot replace ???
with john
because
john
is in the process to be completed—it is not completed yet so we cannot refer back to john
.
This feels like a "chicken and egg problem". In reality however we create students and courses and then once a student registers for the class then we update the existing student and course to reflect that now the student has registered for this course.
We can have a similar process in our Java code. In order to do so however we need our language to allow us to create instances of a class that are "incomplete" and need to be filled in at a later point. In Java, and in other languages, we have a special value that we can use as a temporary place holder, null
.
null
null
in Java is a special reference value.
-
it is like an object, but, can take any reference type
-
it has no methods, you cannot call anything on
null
, attempting to call a method onnull
results in aNullPointerException
(NPE) -
it is the default value given to a field/variable of a reference type if it is not initialized
We can thus pass the null
value for the appropriate argument in our constructors. [1]
Student john = new Student("John", "Hart", 1234, null);
Course introToLogic = new Course(333, "Intro to Logic", null);
Using null
as a temporary value lets us create our instances for Student
and Course
but how do we then update the fields course
and student
for Student
and Course
objects respectively? The answer is to use setter methods.
Setter Methods
A setter method is a method whose responsibility is to update or set a field to a new value without returning a new instance.
/**
* Represents a course in our registration system for the school.
*/
public class Course {
private Integer courseNumber;
private String title;
private Student student;
public Course(Integer courseNumber, String title, Student student) {
this.courseNumber = courseNumber;
this.title = title;
this.student = student;
}
/**
* Setter for property 'courseNumber'.
*
* @param courseNumber Value to set for property 'courseNumber'.
*/
public void setCourseNumber(Integer courseNumber) { (1)
this.courseNumber = courseNumber; (2)
}
/**
* Setter for property 'title'.
*
* @param title Value to set for property 'title'.
*/
public void setTitle(String title) {
this.title = title;
}
/**
* Setter for property 'student'.
*
* @param student Value to set for property 'student'.
*/
public void setStudent(Student student) {
this.student = student;
}
// getter methods elided.
}
1 | The return type of setter methods is void . The type void in Java is used for methods that return no value to the caller.
Java methods with the return type void mutate (alter/update) the internal state of your objects. |
2 | The body of our getters is similar to the body of our constructors; we set the appropriate field to the value provided in the getter method’s argument. |
The naming convention for setter methods is to prepend the word set to the field name using CamelCase, e.g. setStudent
|
Let’s now complete our example and finish our creation of john
and introToLogic
Student john = new Student("John", "Hart", 1234, null);
Course introToLogic = new Course(333, "Intro to Logic", null);
john.setCourse(introToLogic); (1)
introToLogic.setStudent(john); (2)
1 | set the object introToLogic as the value for the field course inside the object john . This line of code connects our john object to our introToLogic object that satisfies the arrow in our UML Class diagram from Student to Course |
2 | set the object john as the value for the field student inside the object introToLogic . This line of code connects our introToLogic object to our john object that satisfies the arrow in our UML Class diagram from Course to Student |
Testing void
methods
When it comes to testing methods that return void
our current pattern of testing does not work.
Currently all of our tests would call a method and the method would return back to us a value.
Our assert
statements would then check that the value returned equals an expected value that
we have constructed in our test class.
With void
methods however there is no value returned from our method call. Instead the void
method
has altered the object that it was called on and changed its state.
So since calling a void
method changes the state of the object it is called on, our test will have
to check that the change due to the void
method altered the object in exactly the way we expected.
Let’s take a simpler class, here is Posn
again but this time, with setter methods.
public class Posn {
private Integer x;
private Integer y;
public Posn(Integer x, Integer y) {
this.x = x;
this.y = y;
}
/**
* Getter for property 'x'.
*
* @return Value for property 'x'.
*/
public Integer getX() {
return x;
}
/**
* Setter for property 'x'.
*
* @param x Value to set for property 'x'.
*/
public void setX(Integer x) {
this.x = x;
}
/**
* Getter for property 'y'.
*
* @return Value for property 'y'.
*/
public Integer getY() {
return y;
}
/**
* Setter for property 'y'.
*
* @param y Value to set for property 'y'.
*/
public void setY(Integer y) {
this.y = y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o != null && getClass() != o.getClass()) return false;
Posn posn = (Posn) o;
if (x != null ? !x.equals(posn.x) : posn.x != null) return false;
return y != null ? y.equals(posn.y) : posn.y == null;
}
@Override
public int hashCode() {
int result = x != null ? x.hashCode() : 0;
result = 31 * result + (y != null ? y.hashCode() : 0);
return result;
}
}
And here is the test
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class PosnTest {
private Posn p11;
private Posn p10;
private Posn p01;
@Before
public void setUp() throws Exception {
this.p11 = new Posn(1,1);
this.p10 = new Posn(1,0);
this.p01 = new Posn(0,1);
}
@Test
public void getX() throws Exception {
Assert.assertEquals(new Integer(1), this.p11.getX());
}
@Test
public void setX() throws Exception {
Posn p1001 = new Posn(10,1); (1)
p1001.setX(1); (2)
Assert.assertEquals(p11, p1001); (3)
}
@Test
public void getY() throws Exception {
Assert.assertEquals(new Integer(1), this.p11.getY());
}
@Test
public void setY() throws Exception {
Posn p0110 = new Posn(1,10);
p0110.setY(0);
Assert.assertEquals(p10, p0110);
}
}
1 | We create a new Posn that we are going to mutate calling one of its void methods, in this test setX() |
2 | We call setX() and now the object stored under the variable p1001 has been mutated (modified in place) |
3 | Now we assert that our p1001 is the same as p11 . |