Professional Documents
Culture Documents
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.
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:
// 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
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 .
// 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() {
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
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.
Since Circle is both a Shape and Scalable , we can then do the following assignments:
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.
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.
Spectrum of classes
Concrete Class (Eg. Point , Circle , Rectangle ):
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).
Since there are many implementations of the List interface, like ArrayList and LinkedList , these kinds of assignment is possible:
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:
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.
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
Return a negative integer when the first integer is smaller than 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.
If we wanted to sort integer in descending order instead of ascending order, there will be 2 ways to go about it:
Comparators can not only sort values of integers, but can also sort the areas of shapes.
Comparator class definition:
Sorting code:
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
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:
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)