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.
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.
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
-
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
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 elements1
,2
and3
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 variablex
, followed by 0 or more numbers, denoted byy, …
. 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 methodm()
is called and the result of the method call is the valuer
-
((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 methodm()
is called with 2 arguments where-
the first method argument is the same as the first element of the list, namely
x
-
the second method argument is a value
p
that may or may not be in the list. -
the result of the method call is the value
r
-
-
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
* @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;
}
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!");
}
@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{}";
}
}
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
+ '}';
}
}
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
, or the classAList
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
, or the class AList
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 to break and adds work for our clients. -
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
* @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
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());
}
/**
* 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 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
.
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
-
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.
-
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
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.
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.
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.
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.
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.
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.