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

  1. data

  2. 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 create 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

  1. describe the conditions that we expect to hold true before calling a method. Conditions can refer

    1. the state of our object

    2. impose restrictions to the values passed as arguments

  2. describe the conditions that we expect to hold true as soon as the method returns. Conditions can refer

    1. to the return value

    2. the relations of the return value and the original values passed as arguments

    3. 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

  1. the client, code that calls a method

  2. the implementation, the body of the method being called.

Software contracts are further subdivided into

  1. The formulae that describes the conditions we expect to hold before calling a method is called a pre-condition.

  2. 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

Empty(): List

Empty() = (())

  1. pre-condition : true

  2. post-condition : (())

isEmpty() : Boolean

(()).isEmpty() = true
((1,2,...)).isEmpty() = false
  1. pre-condition : true

  2. post-condition :

result ->  this.equals( (()) ) AND
!result -> !this.equals( (()) )

add(Integer ele) : List

(()).add(1) = ((1))
((1,2,...)).add(0) = ((0,1,2,...))
  1. pre-condition : true

  2. post-condition :

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(Integer ele) : Boolean

(()).contains(1) = false
((1,2,...)).contains(1) = true
((1,2,...)).contains(x) = ((2,...)).contains(x)
  1. pre-condition : true

  2. post-condition :

result ->
 (this.size() > 0  &&
  exists x (
   1 <= x <= this.size() &&
   this.elementAt(x).equals(ele)))

size() : Integer

(()).size() = 0
((1,2,...)).size() = 1 + ((2,...)).size()
  1. pre-condition : true

  2. post-condition :

result == |this|

elementAt(Integer index) : Integer

(()).elementAt(n) = ERROR
((1,2,...)).elementAt(1) =  1
((1,2,...)).elementAt(n) =  ((2,...)).elementAt(n - 1)
  1. pre-condition : 1 ⇐ index ⇐ this.size()

  2. post-condition :

this.contains(result) AND
this.tail()…​ n-1 .elementAt(1).equals(result)

tail() : List

(()).tail() = ERROR
((1,2,...)).tail() =  ((2,...))
  1. pre-condition : !isEmpty()

  2. post-condition :

result.size() + 1 == this.size() AND
result.add(old.elementAt(0)).equals(old)

Thinking through an example

Class diagram for `List` and `Student` including packages. Package local classes are denoted in gray.
Figure 1. Class diagram for 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

  1. keep the sets the same; no alterations to the contracts of each operation, the client code should work as expected

  2. 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.

  3. 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

  4. 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

liskov

The logical implications that we need to ensure hold true are

  1. A.m preB.m pre and

  2. B.m postA.m post

The implications extend to longer inheritance chains.

liskov multiple

Consider the implications that need to hold at D

  1. A.m preB.m preC.m preD.m pre and

  2. D.m postC.m postB.m postA.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.

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 to 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();
}

Multidimensional arrays

We can have a matrix (2-dimensional) arrays.

 Integer[][] matrix = new Integer[5][10]; // 5 arrays, each array with 10 elements

or even more 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 uses as the 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 for true and false

  • 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

course

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.

  1. it is like an object, but, can take any reference type

  2. it has no methods, you cannot call anything on null, attempting to call a method on null results in a NullPointerException (NPE)

  3. 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

1. We will see other ways to allow partial creation of our instance later with constructor overloading.