Relevant Reading Material
Chapters From How to Design Classes (HtDC)
-
Chapter 18
-
Chapter 19
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 code module (typically a class in our case).
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 implementor views.
Person
Example
Recall the Person
example from our first lecture.
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
,
-
Person
is the implementation of the concept of a person. -
PersonTest
is a client ofPerson
.
Author
Example
We are using these two views of our code when we write our templates. Recall the template from lecture 3 for Author
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 diagramLet’s consider our Cons
class.
-
it is a client to
List
, the fieldrest
is aList
and some of the methods inCons
userest
as a client -
it is a client to
Integer
, the fieldfirst
is anInteger
and some of the methods inCons
usefirst
as a client -
it is an implementation for
List
, one of the possible instances of aList
is aCons
and it has access to internal information likefirst
andrest
as well as possibleprivate
/protected
methods available only toList
implementations. But is also has access topublic
methods ofCons
which is aList
so it is also a client of List.
Our classes take up many roles.
Defining an ADT
ADT are writen 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
-
the expected inputs and any conditions our inputs and/or our ADT must hold
-
the expected outputs and any conditions our output and/or our ADT must hold
-
invariants about our ADT
Let’s write our ADT for a List
Operation | Specification | Comments |
---|---|---|
|
|
Creates an empty list |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
-
Do the right thing!
-
understand what each operation should do.
-
-
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
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
*/
List tail();
}
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);
}
}
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!");
}
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{}";
}
}
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);
}
}
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
+ '}';
}
}
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);
}
}
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.
List
and Student
including packages.Note that,
-
All classes in
edu.neu.ccs.cs5004.listadtv1
are accesible from the packageedu.neu.ccs.cs5004.listadtclient1
, including classes that are internal, e.g.,Cons
andEmpty
-
Classes found outside of the package
edu.neu.ccs.cs5004.listadtv1
also have access to methods defined on internal classes, e.g.,getFirst()
onCons
Let’s have a look at the implementation of Student
and how we can create a Student
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
+ '}';
}
}
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
.
Recall our 2 rules
-
Single Responsibility.
-
Law of Demeter; talk to your friends.
Changes to the internal implementation of List
can cause compile time errors to all List
clients.
-
Renaming
Empty
orCons
causes all our clients work. -
Altering our implementation of
List
in order to not useCons
andEmpty
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 |
---|---|
|
|
|
|
|
|
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.
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
*/
List tail();
}
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
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.
-
An interface has a more direct mapping to an ADT.
-
no information on data structure
-
no fields or data inherited in your code when we extend an interface
-
all implementing classes have to provide an implementation for each method
-
-
Clients are only aware of operations available because of the interface.
-
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
-
Blackbox testing
-
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
-
removeAll(List toBeRemoved) : List
which consumes a list of elements calledtoBeRemoved
and returns a new list with all the elements intoBeRemoved
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
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);
removeAll
class Empty extends AList {
// ... elided code ...
@Override
public List removeAll(List toBeremoved) {
return this;
}
}
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.
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]
removeAll
class Cons extends AList {
// ... elided code ...
@Override
public List removeAll(List toBeremoved) {
return removeAllAcc(toBeremoved, new Empty());
}
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
-
Abstract methods can only exist inside abstract classes.
-
A concrete (non-abstract) class
C
that extends an abstract classA
must provide implementations for each abstract method inA
. -
An abstract class
D
that extends an abstract classA
can optionally provide implementations for abstract methods inA
. If it does not, the responsibility to provide implementations for abstract methods inA
andD
is delegated to all concrete subclasses ofD
.
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
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
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
-
The number of arguments of
A.m
andB.m
is the same. -
For each argument type, in order,
-
if
T1
is a primitive type,T2
must be the same primitive type. -
if
T1
is a reference type,T2
must be the same reference type.
-
-
For the return type
-
if
R1
is a primitive type,R2
must be the same primitive type. -
if
R1
is avoid
,R2
must bevoid
. -
if
R1
is a reference type,R2
must be the same type or subtype ofR1
.
-
Also, the overriding method B.m
cannot decrease the accessibility of A.m
, e.g.,
-
if
A.m
is declaredpublic
B.m
cannot declare beprivate
orprotected
or default.
B.m
however may increase the accessibility of the overridden method, e.g.,
-
if
A.b
is declaredprotected
,B.m
can leave it asprotected
or make itpublic
.