You are on page 1of 6

Week 4: Contract between classes

Shapes
In the previous lecture, we went though the inheritance (”is-a”) relationship of Circle and FilledCircle . Now, say we introduced another class Rectangle , this class will be
related to Circle as they are both shapes. Thus, one might model the relationship between Circle and Rectangle , and a new class Shape as another inheritance
relationship ( Circle is a Shape , Rectangle is a Shape .)

Circle and Rectangle are concrete objects that are constructible with defined properties (Eg. Every Circle has a centre and radius, Every Rectangle has a length and
width). But however, Shape is abstract as it can have many sides, can have a radius, length etc, and its properties are undefined.

Shapes as an abstract class


The keyword abstract means that Shape cannot be instantiated as an object, as it would not make any sense because its properties and constructor method are undefined.
(When we compile and run new Shape() , the code will not compile.)

Shape is a parent abstract class that has the child classes Circle and Rectangle , and under Shape 's class definition, it should include all the methods that is common to and
overwritten by its child classes. In this case, it would contain the method getArea() .

An example of the abstract class modelling of Shape together with Circle and Rectangle is shown below:

// Shape abstract class


abstract Class Shape {
double getArea() {
return 1.0;
}
}

// Circle class
Class Circle extends Shape {
...
@Override
double getArea() {
return Math.PI * this.radius * this.radius;
}
...
}

// Rectangle class
Class Rectangle extends Shape {
...
@Override
double getArea() {
return this.length * this.width;
}
}

Shapes as an interface
Since shape is an abstract class, it means it cannot be instantiated, thus it is meaningless to include an implementation of getArea() defined under it there would never
exist any Shape object that can call its implementation with a run time type of Shape .

Thus, we can remove the implementation (method body) of getArea() defined under the Shape class ( return 1.0 is meaningless). We can do this by declaring Shape as an
interface instead of an abstract class, as a means of a contract to enforce the child classes to override the implementation of the getArea() method. Also, we have to make

the getArea() method under the interface Shape to be abstract .

Now, Circle and Rectangle will no longer be inheriting from Shape as Shape is no longer a class. Thus, we will use the keyword implements instead of extends .

Also, the method access in an interface is always implicitly public , since the access in the child classes cannot be more restrictive than the parent class, all the methods
common to the interface which are being overwritten in the child classes will need to be declared public .

Like an abstract class, an interface is also non-instantiable

An example of an interface modelling of Shape with abstract methods is shown below:

// Shape interface
interface Shape {
abstract double getArea(); // Adding abstract in front of methods is optional
}

// Circle class
Class Circle implements Shape {
...
@Override
public double getArea() {
return Math.PI * this.radius * this.radius;
}
...
}

// Rectangle class
Class Rectangle implements Shape {
...
@Override
public double getArea() {

Week 4: Contract between classes 1


return this.length * this.width;
}
}

Implementing multiple interfaces


In addition to Shape , another interface that we can add is Scalable , which determines whether an object can be scaled or not. Any classes that fulfil the Scalable contract
(ie. implements Scalable ) will have the ability to be scaled.

We can implement the interface Scalable together with the interface Shape as shown below:

// Shape interface
interface Shape {
abstract double getArea();

// Scalable interface
interface Scalable {
abstract Scalable scale(int factor);
}

// Circle class
Class Circle implements Shape, Scalable {
...
@Override
public double getArea() { // implementing getArea() from Shape
return Math.PI * this.radius * this.radius;
}

@Override
public Circle scale(int factor) { // implementing scale() from Scalable
return new Circle(this.centre, this.radius * factor)
}
}

// Rectangle class
Class Rectangle implements Shape, Scalable {
...
@Override
public double getArea() {
return this.length * this.width;
}

@Override
public Rectangle scale(int factor) {
return new Rectangle(this.length * factor, this.width * factor)
}
}

Basically, every object that implements the interface Scalable has to have a scale() method defined under their class definition. The scale() method will return another
Scalable , which is a new scaled object, and will take in a scale factor of integer data type as argument.

Also note that for the scale() method, the return type of the overridden method defined under Scalable is different from the overriding method defined under the Circle

or Rectangle class. This is okay as a Circle or Rectangle is a Scalable .

The general rule of thumb is that the return type of an overriding method CANNOT be more general (eg. cannot return a superclass objects) but can be more specific
(can return the same class or its subclass objects) than the return type of the overwritten method.

Is-a relationship revisited


Just like how a subclass is substitutable for its superclass (Substitutability principal), an implementation class is also substitutable for its interface.

Since Circle is both a Shape and Scalable , we can then do the following assignments:

Shape s = new Circle(new Point(1.0, 1.0), 1.0)

Scalable k = new Circle(new Point(1.0, 1.0), 1.0)

These above assignments will leverage on the principal of substitutability to determine the methods that variables s and k can call as well as their actual behaviours
when the methods are called. In the case above, the compile time type of the variables will be Shape and Scalable respectively, and both their run time types will be
Circle .

Thus, k.getArea() would not compile as the compile time type is Scalable and there is no getArea() method defined under the Scalable class. Similarly, s.scale(2)

cannot be called.

Inheriting multiple parent classes


As mentioned before, a particular class can implement from multiple interfaces. But, a particular class can only inherit from ONE parent class.

The reason is because if there is a method in BOTH parent classes with the exact same method signature but a different implementation, that method would be inherited
from both the parent classes to the child class. Assuming the method is not meant to be overridden, there will be ambiguity in the behaviour of the method when that
method is called on the child class. (Will the method follow parent class 1 ‘s implementation or parent class 2’s implementation?). Note that method overloading would not
apply here due to the same method signature.

Week 4: Contract between classes 2


For interfaces, this problem would not exist as merely the method signature is defined under the interface and not the implementation. More importantly, methods stated in
the interfaces are purely to enforce the use of these methods by implementers but not to call the methods itself. Thus, interface methods are meant to be overwritten by
implementers, and the behaviour of the method will follow that of the implementer. (Similarly, an interface can inherit from multiple parent interfaces)

Spectrum of classes
Concrete Class (Eg. Point , Circle , Rectangle ):

Has defined properties

Has defined methods (with method body)

abstract Class (Eg. FilledShape ):

An intermediate between concrete classes and interfaces

interface (Eg. Shape , Scalable ):

ONLY has method signatures (no method body)

The FilledShape class is an abstract class as it has defined properties which is color, and has a constructor too. But even though it has a constructor, it cannot be
instantiated as it has an abstract method, which is getArea() . This method is abstract as it has no method body, because of the fact that the method can be called on any
FilledShape object, which have no defined properties other than color, thus it would be impossible to specify a particular implementation. The method implementation will

thus be dependent on the implementation of the class which will be passed as the argument to the function (run-time type), which will be the child class of the abstract class
(substitutability principal).

Java List interface


An important aspect of interfaces is that the client only cares about the interface itself and not its implementors. (classes who implements the interface) The client is only
concerned with which methods it can call and use but does not want to know the underlying implementations of the methods and how the methods actually behaves when
called.

Since there are many implementations of the List interface, like ArrayList and LinkedList , these kinds of assignment is possible:

List<Integer> list = new ArrayList<Integer>()

List<Integer> list = new LinkedList<Integer>()

The method List.of() is a factory method which provides an alternative way to create a List object without using the new keyword and constructor. However, using the
List.of() actually creates a new AbstractImmutableList object which is immutable.

List<Integer>.of(1,2,3)

The code below will generate an AbstractImmutableList of [1,2,3], which allows read-access (methods that does not change the list) but not write-access (methods that
changes the list).

Thus doing List<Integer> list = List.of(1,2,3) and then followed by list.add(4) will compile because the compile time type is List and the add() method can be
called, but it will raise an exception because its run-time type is AbstractImmutableList , and there is no add() method defined under the AbstractImmutableList class.

This List.of() method can be an alternative way to create an immutable list, instead of doing:

ImList<Integer> imlist = new ImList<Integer>().add(1).add(2).add(3)

We can now do:

ImList<Integer> imlist = new ImList<Integer>(List.of(1,2,3))

Java collections framework


Basically, Collection is the parent interface for child interfaces like Set , List and Queue . A parent interface or superinterface is just an interface that a subinterface inherits
the method signatures from.

For example, if a parent interface specifies the method foo() , the child interface will implicitly inherit this method specification. If the child interface also specifies another
method bar() that is not defined under the parent interface, the implementor class that implements from the child interface will need to provide implementations for both
foo() and bar() .

It will be useful to know which methods are specified under the parent interface and which methods are specified under the child interface.

Comparators
There are many ways to sort a list, thus one needs to specify HOW to sort a list, which will involve passing some argument into the List.sort() method to control the order
in which the sort is to be done.

Week 4: Contract between classes 3


With reference to the Java API, the List.sort() method takes in a Comparator , which is an interface . Thus to construct your own specific Comparator class, we will need to
implement from the Comparator interface. Also, another important thing is to look at the abstract methods specified by the Comparator interface to be used as a basis for you
to decide on the method implementation of your own Comparator class.

Thus, we will need to create a Comparator class that implements the Comparator interface with that includes the implementations of the compare() method, which is an
abstract method under the method specifications of the Comparator interface. Then, we will need to pass in our own Comparator class that we created as an argument to

List.sort() to sort a list to our preference.

Below is an example of how we define our own comparator classes:

class IntComp implements Comparator<Integer> {


@Override
public int compare(Integer i, Integer j) {
return i - j;
}
}

As mentioned in the Java API specification, a comparator should:

Return a negative integer when the first integer is smaller than the second

Return 0 when the first integer is equals to the second

Return a positive integer when the first integer is larger than the second

The returned integer can be any value as long as the sign is correct, thus doing i - j will work.

Once we have finished creating our Comparator class, which is this case is IntComp , we can then proceed to sort an ArrayList , LinkedList or even ImList , as well as other
implementors of the List interface.

jshell> ImList<Integer> imlist = new ImList<Integer>(List.of(3,2,1))


jshell> imlist.sort(new IntComp())
==> [1,2,3]

If we wanted to sort integer in descending order instead of ascending order, there will be 2 ways to go about it:

// 1st way: Changing the implementation of the IntComp class


class IntComp implements Comparator<Integer> {
@Override
public int compare(Integer i, Integer j) {
return j - i; // i - j is changed to j - i
}
}

// 2nd way: Keeping implementation of IntComp class and using .reversed()


list.sort(new IntComp().reversed())

Comparators can not only sort values of integers, but can also sort the areas of shapes.
Comparator class definition:

Sorting code:

Week 4: Contract between classes 4


Iterator interface
An Iterator interface will provide an alternative to loop through a List or any other iterable.

Usually, to loop though any iterable, we will use an enhanced for loop:

jshell> ImList<Shape> shapes = new ImList<Shape>(List.of(new Circle(new Point(1.0, 1.0), 1.0), new Rectangle(2, 3));
jshell> for (Shape s : shapes) { // Enhanced for loop
System.out.println(s.getArea());
}
==> 1.0
6.0

To use the Iterartor interface to iterate through a List , we will need to do the following steps:

jshell> ImList<Shape> shapes = new ImList<Shape>(List.of(new Circle(new Point(1.0, 1.0), 1.0), new Rectangle(2, 3));
jshell> Iterator<Shape> iter = shapes.iterator(); // Creating an iterator implementation
jshell> while (iter.hasNext()) {
System.out.println(iter.next().getArea());
}
==> 1.0
6.0

The important abstract methods of the Iterator interface is shown below:

Lab 2 learning points


Single responsibility principle: The general idea is that every class, module or function in a program should have exactly one purpose in the program. Every class, module or
function should have only ONE reason to change.

Use of guard clauses

Guard clauses are generally just if statements that protects the method from any exceptions by immediately returning the method or throwing an exception so that the
rest of the method below would not have to worry about the condition anymore (It is a great alternative as compared to many many nested if-else statements)

An example of the use of guard clauses in the place of many nested if-else statements is shown below:

// CreateUser() using nested if-else statements


public void CreateUser(String username, String password) {
if (username == null) {
return throwUserException();
} else {
if (password == null) {
return throwPasswordException();
} else {
if (password.length() < 8) {
return throwPasswordLenghtException();
} else {
System.out.println("User Created");
}
}
}
}

// CreateUserBetter() using guard clauses


public void CreateUserBetter(String username, String password) {
if (username == null) {
return throwUserException();
}

if (password == null) {
return throwPasswordException();
}

if (password.length() < 8) {
return throwPasswordLenghtException();
}

System.out.println("User Created");
}

Modularisation; It is always good to break up lengthy methods into shorter ones that ultimately have the same functionality. For example, one can abstract out lines of code
in a method into helper methods (it is okay even if the helper method is just called once)

Week 4: Contract between classes 5


When a class both inherits from a superclass and implements multiple interfaces, the extends keyword will be placed before the implements keyword, and the multiple
interfaces are arranged from left to right in alphabetical order:

class FilledCircle extends Circle implements Printable, Scalable, Shape {


...
}

Week 4: Contract between classes 6

You might also like