Relevant Reading Material

The Client Vs Implementer view

You have been taking up the role of both client and implementor all along. Lets take a few examples and identify client and implementer views.

Person Example

Recall the Person example from our first lecture.

Class Diagram for `Person`
Figure 1. Class Diagram for Person

For a piece of code c (class or method) that relies on the public (external) elements of another class K we identify c as a client of K and the code inside K that has access to private (internal) information for K as the implementation of K

So looking at the two classes Person and PersonTest,

  1. Person is the implementation of the concept of a person.

  2. PersonTest is a client of Person.

Author Example

We are using these two views of our code when we write our templates. Recall the template from lecture 3 for Author

author-template
public class Author {
  Person person;
  Email email;
  Address address;


//    Template for a no-argument method


//     ??? template() {
//
//      ... this.person ...  // Person                               (1)
//      ... this.email  ...  // Email                                (2)
//      ... this.address ... // Address                              (3)
//      ... this.mmm() ...   // any method on `this`                 (4)
//
//      ... this.person.mmm() ...  // any public method on Person    (5)
//      ... this.email.mmm() ...   // any public method on Email     (6)
//      ... this.address.mmm() ... // any public method on Address   (7)
//    }

}
1 code that uses internal information to Author, thus implementation of Author
2 code that uses internal information to Author, thus implementation of Author
3 code that uses internal information to Author, thus implementation of Author
4 code that uses internal/external information to Author, internal when we are calling private, protected methods external when we are calling public methods
5 code that uses external information to Person, thus client of Person
6 code that uses external information to Email, thus client of Email
7 code that uses external information to Address, thus client of Address

As developers we take up the role of client and developer while we are designing our programs.

List Example

Recall our List example

`List` class diagram
Figure 2. List class diagram

Let’s consider our Cons class.

  • it is a client to List, the field rest is a List and some of the methods in Cons use rest as a client

  • it is a client to Integer, the field first is an Integer and some of the methods in Cons use first as a client

  • it is an implementation for List, one of the possible instances of a List is a Cons and it has access to internal information like first and rest as well as possible private/protected methods available only to List implementations. But is also has access to public methods of Cons which is a List so it is also a client of List.

Our classes take up many roles.

Abstract Data Types (ADTs)

  • An Abstract Data Type (ADT) refers to a model that describes data by specifying the operations that we can perform on them.

    • A model here refers to a conceptual model which is composed of concepts used to help us know, understand/simulate a subject that the model represents. We will use pseudocode and concepts from mathematics and logic to define our models.

  • A Data Structure refers to the concrete implementation (the details).

  • Clients care about the ADT.

  • Developers care about

    • the data structure (it’s memory consumption, speed, correctness) and

    • how the inner workings of the data structure faithfully implement the ADT.

This is why you will often here about the public interface and the private interface of a code module (typically a class in our case).

Defining an ADT

ADTs are written from the view of the client in that we need to capture the clients expectations in terms of the operations on the ADT. For each operation we need to describe

  1. the expected inputs and any conditions our inputs and/or our ADT must hold

  2. the expected outputs and any conditions our output and/or our ADT must hold

  3. invariants about our ADT

The ADT needs to specify, in detail, what is required by the client calling the operations of the ADT and what is expected as a result if the operations are called as specified. In order to specify these details, an ADT specification typically uses a language (format) that is general and allows us to specify the necessary conditions.

The language used for specifications in a ADT are typically

  • mathematical notation

  • pseudocode

  • English

  • or a mixture of the above

Let’s write our ADT for a List. We will use

  • double parenthesis to denote a list. For example

    • (()) is what we will write to denote the empty list

    • (( 1, 2, 3 )) is what we will write to denote the list with elements 1, 2 and 3 in that order.

    • ((x, y, …​)) is what we will write to denote a list of one or more elements. The first element is some number, denoted by the variable x, followed by 0 or more numbers, denoted by y, …​. The ellipsis …​ mean 0 or more repetitions of the element found immediately before the …​.

    • ((x, y,…​)).m() ⇒ r is what we write to denote what happens to a non-empty list with at least one element, when the method m() is called and the result of the method call is the value r

    • ((x, y,…​)).m(x,p) ⇒ r is what we write to denote what happens to a non-empty list with at least one element, when the method m() is called with 2 arguments where

      1. the first method argument is the same as the first element of the list, namely x

      2. the second method argument is a value p that may or may not be in the list.

      3. the result of the method call is the value r

Operation Specification Comments

new Empty()

new Empty() ⇒ (())

Creates an empty list

isEmpty() : Boolean

(()).isEmpty() => true
((1,2,...)).isEmpty() => false
  • Calling isEmpty() on an empty list returns true

  • Calling isEmpty() on a non-empty list returns false

first() : Integer

(()).first() => ERROR
((x,y,...)).first() => x
  • Calling first() on an empty list throws an error

  • Calling first() on a non-empty list returns the first element in the list.

rest() : List

(()).rest() => ERROR
((x,y,...)).rest() => ((y,...))
  • Calling rest() on an empty list throws an error.

  • Calling rest() on a non-empty list returns the tail of the list, all elements except the first one in their original order.

add(Integer ele) : List

(()).add(x) => ((x))
((x,y,...)).add(z) => ((z,x,y,...))
  • Calling add(e) on an empty list returns a one-element list with the element e

  • Calling add(e) on a non-empty list returns the original list with e pre-pended

contains(Integer ele) : Boolean

(()).contains(x) => false
((x,y,...)).contains(x) => true
((x,y,...)).contains(z) => ((y,...)).contains(x)
     where x != z
  • Calling contains(e) on an empty list returns false

  • Calling contains(e) on a non-empty list returns true if the element is in the list, false otherwise

size() : Integer

(()).size() => 0
((x,y,...)).size() => 1 + ((y,...)).size()
  • Calling size() on an empty list returns 0

  • Calling size() on a non-empty list returns the total number of element in the list

elementAt(Integer index) : Integer

(()).elementAt(n) => ERROR
((x,y,..)).elementAt(n) => ERROR
      if ((x,y,...)).size() < n
((x,y,...)).elementAt(1) =>  x
((x,y,...)).elementAt(n) =>  ((y,...)).elementAt(n - 1)
  • Calling elementAt(n) on an empty list is an error

  • Calling elementAt(n) on a non-empty list returns the element at index n

The specification is a logical/mathematical description of what the operation does using the operations inputs and outputs. It is not a specification on how the operations achieves the desired output.

  • The clients only cares about what each operation does.

  • The developer needs to

    1. Do the right thing!

      • understand what each operation should do.

    2. Do the thing right!

      • design a program that implements the specification correctly.

Implementing the List ADT, first attempt.

Let’s use our pattern to implement this ADT for List

List.java
package edu.neu.ccs.cs5004.listadtv1;

/**
 * Represents a List of Integers.
 */
public interface List {

  /**
   * Check if the list is empty.
   *
   * @return true if list is empty, false otherwise
   */
  Boolean isEmpty();

  /**
   * Prepend {@code element} to this list.
   *
   * @param element new element to be added
   * @return same list with element prepended
   */
  List add(Integer element);

  /**
   * Check if {@code element} is in the list.
   *
   * @param element the element we are looking for
   * @return true if element is in the list, false otherwise
   */
  Boolean contains(Integer element);

  /**
   * The number of elements in this list.
   *
   * @return number of elements in this list
   */
  Integer size();

  /**
   * Retrieve the element at {@code index}. Index should be greater or equal to 1
   * and less than or equal to the list's size.
   *
   * @param index position in the list
   * @return element at position index
   * @throws IncorrectIndexException  when index is less than 1 or greater than size()
   */
  Integer elementAt(Integer index) throws IncorrectIndexException;

  /**
   * Return the tail of this list; all elements except the first one.
   *
   * @return the same list without the first element
   * @throws IllegalOperationException when called on an empty list
   */
  List rest() throws IllegalOperationException;


  /**
   * Return the first element of the list.
   *
   * @return the first element
   * @throws IllegalOperationException when called on an empty list
   */
  Integer first() throws IllegalOperationException;

}
AList.java
package edu.neu.ccs.cs5004.listadtv1;

/**
 * Represents an abstract list.
 *
 */
public abstract class AList implements List {

  public List add(Integer element) {
    return new Cons(element, this);
  }
}
Empty.java
package edu.neu.ccs.cs5004.listadtv1;

/**
 * Represents an empty list.
 *
 */
 class Empty extends AList{

  public Empty() {}

  public Boolean isEmpty() {
    return true;
  }

  public Boolean contains(Integer element) {
    return false;
  }

  public Integer size() {
    return 0;
  }

  public Integer elementAt(Integer index) throws IncorrectIndexException {
    throw new IncorrectIndexException("Index out of bounds!");
  }

  @Override
  public List rest() throws IllegalOperationException {
    throw new IllegalOperationException("Called rest on empty list.");
  }

  @Override
  public Integer first() throws IllegalOperationException {
    throw new IllegalOperationException("Called first on empty list.");
  }

  public List tail() {
    throw new IllegalOperationException("Called tail on empty list.");
  }


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

  @Override
  public int hashCode() {
    return 42;
  }

  @Override
  public String toString() {
    return "Empty{}";
  }
}
Cons.java
package edu.neu.ccs.cs5004.listadtv1;

import java.util.Objects;

/**
 * Represents a list of at least one element.
 */
public class Cons extends AList {

  private Integer first;
  private List rest;

  public Cons(Integer first, List rest) {
    this.first = first;
    this.rest = rest;
  }


  public Boolean isEmpty() {
    return false;
  }

  public Boolean contains(Integer element) {
    if (getFirst().equals(element)) {
      return true;
    } else {
      return getRest().contains(element);
    }
  }

  public Integer size() {
    return 1 + getRest().size();
  }

  public Integer elementAt(Integer index) throws IncorrectIndexException {
    if (index < 1 || index > size()) {
      throw new IncorrectIndexException("Index out of bounds.");
    }

    if (index.equals(1)) {
      return getFirst();
    } else {
      return getRest().elementAt(index - 1);
    }
  }

  @Override
  public List rest() throws IllegalOperationException {
    return this.getRest();
  }

  @Override
  public Integer first() throws IllegalOperationException {
    return this.getFirst();
  }

  public List tail() {
    return getRest();
  }

  /**
   * Getter for property 'rest'.
   *
   * @return Value for property 'rest'.
   */
  public List getRest() {
    return rest;
  }

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


  @Override
  public boolean equals(Object other) {
    if (this == other) {
      return true;
    }
    if (other == null || getClass() != other.getClass()) {
      return false;
    }
    Cons cons = (Cons) other;
    return Objects.equals(first, cons.first)
        && Objects.equals(rest, cons.rest);
  }

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

  @Override
  public String toString() {
    return "Cons{"
        + "first=" + first
        + ", rest=" + rest
        + '}';
  }

}
IncorrectIndexException.java
package edu.neu.ccs.cs5004.listadtv1;

/**
 * Represents a situation where the index provided for a given list is out of bounds.
 */
public class IncorrectIndexException extends Exception {

  public IncorrectIndexException(String message) {
    super(message);
  }
}
IllegalOperationException.java
package edu.neu.ccs.cs5004.listadtv1;

/**
 * Represents an illegal operation on a List.
 */
public class IllegalOperationException extends RuntimeException{
  public IllegalOperationException(String msg) {
    super(msg);
  }
}

Using our List ADT implementation

So now that we have an implementation of our List ADT lets try and use it to represent a Student in a class for which we would like to capture their grades over the course of a semester.

Class diagram for `List` and `Student` including packages.
Figure 3. Class diagram for List and Student including packages.

Note that,

  1. All classes in edu.neu.ccs.cs5004.listadtv1 are accesible from the package edu.neu.ccs.cs5004.listadtclient1, including classes that are internal, e.g., Cons and Empty

  2. Classes found outside of the package edu.neu.ccs.cs5004.listadtv1 also have access to methods defined on internal classes, e.g., getFirst() on Cons, or the class AList

Let’s have a look at the implementation of Student and how we can create a Student

Student.java
package edu.neu.ccs.cs5004.listadtclient1;

import java.util.Objects;

import edu.neu.ccs.cs5004.listadtv1.List;

/**
 * Represents a student in a class.
 */
public class Student {
  private Integer studentId;
  private List grades;

  public Student(Integer studentId, List grades) {
    this.studentId = studentId;
    this.grades = grades;
  }


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

  /**
   * Getter for property 'grades'.
   *
   * @return Value for property 'grades'.
   */
  public List getGrades() {
    return grades;
  }

  @Override
  public boolean equals(Object other) {
    if (this == other) return true;
    if (other == null || getClass() != other.getClass()) return false;
    Student student = (Student) other;
    return Objects.equals(studentId, student.studentId)
        && Objects.equals(grades, student.grades);
  }

  @Override
  public int hashCode() {
    return Objects.hash(studentId, grades);
  }

  @Override
  public String toString() {
    return "Student{"
        + "studentId=" + studentId
        + ", grades=" + grades
        + '}';
  }


}
Creating instances of Student
// elided code

Student mary = new Student(1234, new Empty()); // no grades yet for Mary
Student john = new Student(4444, new Cons(90, new Empty())); // one grade for John

The client code for List is now aware of Cons and Empty. Furthermore, the client code can create instance of Cons and Empty and rely on methods that have nothing to do with List, like getFirst() on Cons, or the class AList

Recall our 2 rules

  1. Single Responsibility.

  2. Law of Demeter; talk to your friends.

Changes to the internal implementation of List can cause compile time errors to all List clients.

  1. Renaming Empty or Cons causes all our clients to break and adds work for our clients.

  2. Altering our implementation of List in order to not use Cons and Empty but instead use something else, e.g., depend on 3rd library. [1]

We can do better!

Implementing the List ADT, second attempt.

Java Class modifiers

Thankfully, Java allows us to use modifiers on class definitions to denote the access/visibility of a class from another package.

A class definition can have

  • a public modifier. Java allows any class from the same package or a different package to access this class.

  • no modifier. Java allows any class from the same package to access this class. Classes defined with no modifier are called package local classes.

Given that we would like to allow our clients to only depend on the minimum, necessary, classes and interface in our package we should alter our Cons, Empty and AList to be only accessible from within the same package. For example

Before After
public class AList implements List { ... }
class AList implements List { ... }
public class Empty extends AList { ... }
class Empty extends AList { ... }
public class Cons extends AList { ... }
class Cons extends AList { ... }

This however poses a new problem for our clients. If both Empty and Cons are defined as package local, we cannot import them in our client code and use them to create instances for our list. We have hidden too much.

We need to provide a mechanism for the client to create instances of our List without exposing our internal classes.

Java static

We have been talking about classes and instances and how an instance has members that can be either

  • fields, or,

  • methods.

In order to call a method on an object we have to first create an instance of that class, which we call an object. Also, in order to access one of the fields we need to first create an instance of that class, which we call an object.

Java allows us to have members on classes as well. Class members can be either

  • fields, or,

  • methods.

To define a class member, Java provides the keyword static. Class members also have modifiers for defining their accessibility, the same modifiers you already know, private, public, protected.

The idea is similar to fields and methods on an instance. The difference is that class members are part of the class and not part of the instances of that class.

Let’s look at examples of static fields and methods.

Simplified Math Library
public class MyMathLib {

    
    public static final Double PI = 3.14;(1)

    public static Integer square(Integer val) {  (2)
        return val * val;(3)
    }
}
1 PI here is a constant. It is class field that we declare and initialize to the value 3.14. PI will be created once and it will be part of the class MyMathLib regardless of how many instance of MyMathLib we create (0 or more). The keyword final is provided by Java and enforces that PI once initialized cannot be altered.
2 A static method looks exactly like a normal method. The difference is the keyword static in the method’s declaration header.
3 Inside the body of a static method we can use any Java expressions that we can typically use in our normal methods except any keyword that refers an instance, for example this is not allowed in the body of a static method since a static method is attached to the class and not to an instance.

Static methods can refer to other static methods or static fields. They cannot use

  • this

  • super

Once we have defined a static field/method, how do we call it? Java allows us to call a static member by using the following syntax.

   <ClassName>.<staticFieldName>;        // for static fields
   <ClassName>.<staticMethodName>(...);  // for static methods

Here is some sample code located in the same package as MyMathLib, on how we would use our MyMathLib and it’s static members.

// Inside a method of our Circle class

  Double perimeter = 2 * MyMathLib.PI * getRadius();          // MyMathLib.PI : static field
  Double area = MyMathLib.PI * MyMathLib.square(getRadius()); // MyMathLib.square(...) : static method
Walk over the evaluation of static fields/methods on the whiteboard.

You have been using static fields and methods. Recall

  System.out.println("Hello!"); // out is a static field.
  java.lang.Math.PI;            // PI is a static field.
  java.lang.Math.max(10,20);    // max is a static method.
All your constants from now own must be declared as static final values inside your classes.

Java interfaces can have static members.

So we could add a static method in our List interface that will allow clients to create an empty list. The name for this approach is static factory method, it is our static method used to create (like in a factory) the appropriate instance for our clients.

List interface with static factory method.
package edu.neu.ccs.cs5004.listadtv2;

/**
 * Represents a List of Integers.
 */
public interface List {


  /**
   * Creates a new empty list.
   *
   * @return new empty list
   */
  public static List createEmpty() {     (1)
    return new Empty();                  (2)
  }

  /**
   * Check if the list is empty.
   *
   * @return true if list is empty, false otherwise
   */
  Boolean isEmpty();

  /**
   * Prepend {@code element} to this list.
   *
   * @param element new element to be added
   * @return same list with element prepended
   */
  List add(Integer element);

  /**
   * Check if {@code element} is in the list.
   *
   * @param element the element we are looking for
   * @return true if element is in the list, false otherwise
   */
  Boolean contains(Integer element);

  /**
   * The number of elements in this list.
   *
   * @return number of elements in this list
   */
  Integer size();

  /**
   * Retrieve the element at {@code index}. Index should be greater or equal to 1
   * and less than or equal to the list's size.
   *
   * @param index position in the list
   * @return element at position index
   * @throws IncorrectIndexException  when index is less than 1 or greater than size()
   */
  Integer elementAt(Integer index) throws IncorrectIndexException;

  /**
   * Return the tail of this list; all elements except the first one.
   *
   * @return the same list without the first element
   * @throws IllegalOperationException when called on an empty list
   */
  List rest() throws IllegalOperationException;


  /**
   * Return the first element of the list.
   *
   * @return the first element
   * @throws IllegalOperationException when called on an empty list
   */
  Integer first() throws IllegalOperationException;

}
1 Our method createEmpty() is a public method to allow clients to call it, and, it is also static since we want to call this method when we do not have an instance and we want to acquire one.
2 List is in the same package as Empty and the client gets our compiled code and our Javadocs so they do not get to see the implementation of createEmpty or the definition of the class Empty

So if we were to update our UML Class diagram and only show what is available to our List clients we would get

Class diagram for `List` and `Student` including packages.
Figure 4. Class diagram for List and Student including packages.
Static members are shown as underlined in UML Class Diagrams.

Coding to an Interface

The design rule to remember here is that it is best to code to an interface. The word interface in the preceding sentence refers to the operations that define a system and not to Java’s interface. A Java interface is the language feature that aspires to help developers follow this design rule.

Another possible way of stating the rule to avoid confusion is, code against the ADT.

  1. An interface has a more direct mapping to an ADT.

    1. no information on data structure

    2. no fields or data inherited in your code when we extend an interface

    3. all implementing classes have to provide an implementation for each method

  2. Clients are only aware of operations available because of the interface.

  3. Implementations of the interface can change without altering your clients.

Your ADT should map to a Java interface in your design and there should be a one-to-one mapping between an ADT operation and an interface’s signature.

Testing

There are various types of testing. In this class we will focus on two of those

  1. Blackbox testing

  2. Whitebox (glassbox) testing

Blackbox testing

Blackbox testing refers to tests that test a module as a client (it’s publicly available attributes). For an ADT, this means all the tests that exercise the operations and the specification of each operation found in the ADT to ensure that the code does in fact implement correctly the ADT.

Blackbox tests ensure that the implementation does the right thing.

Changes to the internal implementation of the ADT should not break any of the blackbox tests.

Blackbox tests are written in a separate package from the code being tested.

Whitebox testing

Whitebox testing refers to the tests that exercise the internal implementation of a module, e.g., non-public methods, data structure state during method calls (before and after) internal invariants, etc.

Whitebox tests ensure that the implementation does the thing right.

Changes to the internal implementation of an ADT might and typically do break whitebo testing.

The tests that you have been writing for your assignments are whitebox test.

Helper Methods

Using our list implementation let’s consider the addition of a new operation

  1. removeAll(List toBeRemoved) : List which consumes a list of elements called toBeRemoved and returns a new list with all the elements in toBeRemoved removed from this list. The resulting list can have the elements in any order.

First attempt at removeAll

Here are the additions to our latest implementation of List

List interface additions for removeAll
public interface List {
  // ... elided code ...

  /**
   * Removes all elements of {@code toBeRemoved} from this list.
   *
   * @param toBeremoved elements that we need to remove
   * @return list that does not contain any elements from toBeRemoved
   */
  List removeAll(List toBeremoved);
Empty class additions for removeAll
class Empty extends AList {
  // ... elided code ...

  @Override
  public List removeAll(List toBeremoved) {
    return this;
  }
}
Cons class additions for removeAll
class Cons extends AList {
  // ... elided code ...

  @Override
  public List removeAll(List toBeremoved) {
      if (toBeremoved.contains(getFirst())) {
          return getRest().removeAll(toBeremoved);
      } else {
          return new Cons(getFirst(), getRest().removeAll(toBeremoved));
      }
  }
}

Using an accumulator

Let’s try to re-implement removeAll using an accumulator.

Our interface has the same additional code.

List interface additions for removeAll
public interface List {
  // ... elided code ...

  /**
   * Removes all elements of {@code toBeRemoved} from this list.
   *
   * @param toBeremoved elements that we need to remove
   * @return list that does not contain any elements from toBeRemoved
   */
  List removeAll(List toBeremoved);

What about Cons [2]

Cons class additions for removeAll
class Cons extends AList {
  // ... elided code ...

  @Override
  public List removeAll(List toBeremoved) {
      return removeAllAcc(toBeremoved, new Empty());
  }


  /**
   * Remove all elements of {@code toBeremoved} from this list using an
   * accumulator.
   *
   * @param toBeremoved elements to be removed
   * @param acc elements not in toBeremoved but in the list thus far
   * @return original list without the elements on toBeremoved
   */
  private List removeAllAcc(List toBeremoved, List acc) {
      if (toBeremoved.contains(getFirst())) {
          return getRest().removeAllAcc(toBeremoved, acc);                (1)
      } else {
          return getRest().removeAllAcc(toBeremoved, acc.add(getFirst()); (1)
      }
  }
}
1 Compile-time error!

The compile-time error states that List does not have a method called removeAllAcc. Our code attempts to call removeAllAcc on the result of getRest(). The result of getRest() has a static type of List and List has no method with the name removeAllAcc.

We cannot add removeAllAcc to our List interface. This will break our rule for keeping a one-to-one map with our ADT. Also it breaks another rule, we will be exposing how we decided to solve the remove all operation to our clients; we will be exposing internal implementation details of our List implementation.

Abstract class to the rescue

We can use AList and Java’s abstract methods to allow us to define extra methods to our classes that are only visible to our implementation and thus hidden from our clients.

An abstract method in Java is declared by using the keyword abstract followed by a method signature and a semicolon. This is similar to how we define method signatures in an interface, but, we need to prepend the keyword abstract.

AList adding an abstract class.
  /**
   * Remove all elements of {@code toBeremoved} from this list using an
   * accumulator.
   *
   * @param toBeremoved elements to be removed
   * @param acc elements not in toBeremoved but in the list thus far
   * @return original list without the elements on toBeremoved
   */
  abstract protected List removeAllAcc(List toBeremoved, List acc);

By adding the abstract method removeAllAcc Java forces all concrete classes to provide an implementation for this method. In our examples Cons and Empty must provide an implementation for removeAllAcc.

Java rules for abstract classes and methods
  • Abstract methods can only exist inside abstract classes.

  • A concrete (non-abstract) class C that extends an abstract class A must provide implementations for each abstract method in A.

  • An abstract class D that extends an abstract class A can optionally provide implementations for abstract methods in A. If it does not, the responsibility to provide implementations for abstract methods in A and D is delegated to all concrete subclasses of D.

Let’s continue with our accumulator implementation and focus on Cons a

Cons class additions for removeAllAcc
class Cons extends AList {
  // ... elided code ...

  @Override
  public List removeAll(List toBeremoved) {
      return removeAllAcc(toBeremoved, new Empty());
  }

  @Override
  protected List removeAllAcc(List toBeremoved, List acc) {
      if (toBeremoved.contains(getFirst())) {
          return getRest().removeAllAcc(toBeremoved, acc);                (1)
      } else {
          return getRest().removeAllAcc(toBeremoved, acc.add(getFirst()); (1)
      }
  }
}
1 We are still getting a compile-time error!

The reason for the compile-time error is because rest has a compile-time type of List. We added our new method removeAllAcc to AList not List. We need to change the compile-time type of the field rest.

Here is the change in UML

listafter

Here are the relevant changes in Cons for updating rest to have a compile-time type of AList

Cons changes to make rest of type AList
 class Cons extends AList {

  private Integer first;
  private AList rest;

  public Cons(Integer first, AList rest) { (1)
    this.first = first;
    this.rest = rest;
  }

  public AList getRest() {
      return this.rest;
  }

  /...
}
1 The constructor’s second argument is now of type AList instead of List. AList is-a List based on our subtyping rules.

Finally we need to modify Empty.

Empty class additions for removeAllAcc
class Empty extends AList {
  // ... elided code ...

  @Override
  public List removeAll(List toBeremoved) {
      return this;
  }

  @Override
  protected List removeAllAcc(List toBeremoved, List acc) {
      return acc;
  }
}

We now have added a new helper method removeAllAcc so that our implementation can use it and our clients have no access or view into our accumulator-style solution for the remove all operation.

Java rules for overriding methods

Java follows certain rules in order to decide if one method overrides another method. The rules focus on the types associated with the signature of the methods. For our discussion consider the following UML class diagram

overriderules

We will use A.m to refer to method m in A and B.m to refer to the method m in B. In order for Java to detect/allow an override of method m in A by the method m in B

  1. The number of arguments of A.m and B.m is the same.

  2. For each argument type, in order,

    1. if T1 is a primitive type, T2 must be the same primitive type.

    2. if T1 is a reference type, T2 must be the same reference type.

  3. For the return type

    1. if R1 is a primitive type, R2 must be the same primitive type.

    2. if R1 is a void, R2 must be void.

    3. if R1 is a reference type, R2 must be the same type or subtype of R1.

Also, the overriding method B.m cannot decrease the accessibility of A.m, e.g.,

  • if A.m is declared public B.m cannot declare be private or protected or default.

B.m however may increase the accessibility of the overridden method, e.g.,

  • if A.b is declared protected, B.m can leave it as protected or make it public.

Inheritance

Within a language that supports inheritance, programmers will use inheritance in many different ways. We will briefly introduce some [3] of the common ways that inheritance is used along with some small examples. In the next couple of lectures we will then review these uses of inheritance in the context of Java and introduce some design rules on when to use each of the different uses of inheritance.

The two main motivations for inheritance are

  1. Code Reuse. A child inherits state and behaviour from its parent. We do not have to re-write or copy-paste the code from the parent to the child. The ability of our language that allows us to share or reuse the code simplifies our design and reduces the amount of code in our solutions.

  2. Specification Reuse. A child can override a parent’s behaviour completely. In this case the child did not reuse any code from the parent. The child however is reusing a signature which means that the operation is present in both the parent and the child, however, the parent’s and the child’s behaviour differ.

Let’s consider our List example, but this time add all relevant classes

inheritancefull

Specialization (Subtyping)

This is the one use of inheritance that you have been using in class. The child class is a special case of the parent class in that

  • All operations of the parent are available in the child

  • The behaviour of each operation in the child satisfies the same specification as the corresponding operation in the parent.

This is the case that you have seen in this class. Each method in the child provides the correct implementation for the specification (contract) of the parent, e.g., how we override equals(Object o) and hashCode() for each of our classes. These methods are defined in our parent class Object.

Specification

In this case the child class only inherits the signatures from the parent class. Inheritance here ensures that parent and child have a common set of methods that our code can rely upon, but, leaves the implementation to each child.

We have seen this property in Java with the use of Java’s interfaces. We saw anther case today with abstract classes and abstract methods.

In Java we can have an interface inherit from another interface. Given that our interfaces only contain signature [4] the child interface inherits the signatures of its parent interface.

interfaceinheritance

Construction

In this case the child uses inheritance to reuse the state and behaviour of the parent class but it is not a subtype of the parent, i.e., we cannot use an instance of the child to replace instance of the parent in our code.

Even though this can be done in Java it is typically considered bad design to break the ability to substitute an instance of the parent with an instance of the child.[5]

An example where we use inheritance for construction is when we implement a Set as a child of our List implementation.

inheritanceconstruction

Extension

In this case we would like our child to add new state/behaviour on top of what the parent provides and does not alter the existing behaviour (methods) in a way that contradicts the contracts of the parent class.

These are additional cases that the parent class was not designed for and does not deal with in any way or form.

inheritanceextension

Generalization

In this case we create child to have a more general implementation from the parent. This is the opposite of inheritance for specialization. The child overrides behaviour in the parent in a way that can break the parent’s contracts and because of that, substituting instances of the child for instances of the parent can lead to unexpected bahaviour in existing clients.

This is typically used when the parent was implemented without generalizing and we do not have access to modify the parent code.

You should avoid this case when possible.

inheritancegeneralization

Limitation

In this case we would like to create a child whose behaviour is smaller or more restrictive than the behaviour of the parent. This typically occurs when we do not have access to modify the parent class. This case breaks our ability to use an instance of the child to replace an instance of the parent.

You should avoid this case when possible.

limitation

Combination

In this case inheritance is used so that the child class inherits from a combination of classes. In languages that support multiple inheritance, the child class inherits from 2 or more parent classes, this is a controversial case and is not even allowed by the Java language.

In Java this is not possible through class inheritance. The closest setup to a combination in Java is through the implementation of multiple interfaces.


1. We will see examples of alternative implementations later in the course.
2. We will not use method overloading, yet.
3. This list is no exhaustive. Depending on the language and how the language provides inheritance there can be more uses of inheritance that are not discussed in this course
4. This will change once we start looking at the more recent features added to Java 8
5. More on this topic later, see LSP