Module 10, part 1: More on Class Inheritance and abstract class
es#
In this first part of Module 10 we explore three topics related to class inheritance:
the notions of “abstract class” and “abstract method”;
the similarities ande differences between using interfaces and using inheritance.
Important
To try the examples described in this Module, it is highly recommended to install and use the Java Extension Pack for Visual Studio Code.
The Java Root Class “Object
”#
Consider again the UML class diagram previously shown in Fig. 21, and also shown in Fig. 24 below (for convenience).
By looking at Fig. 24 it might seem
that Pet
is the root class (or base class) of our class hierarchy,
i.e. the class from which all other classes in the hierarchy descend. However,
this is not completely accurate: in fact, in Java, all classes derive from a
root class named Object
(which is typically omitted in UML diagrams).
Therefore, the UML class diagram in Fig. 24
should look, more accurately, as shown in Fig. 25 below.
You can create objects of the class named Object
in the usual way. For
instance, if you write: (try this on the Java shell!)
var o = new Object();
Then you can call e.g. o.toString()
to get a representation of the object as a
String
, or o.equals(o)
to check whether o
is equal to itself.
As expected, the methods of the class named Object
are inherited by the
subclasses — and therefore, if dog
is a variable of type Dog
, you can call
dog.toString()
(try it!). It is quite common for Java classes to
override one or both
methods toString()
and equals(...)
to provide custom implementations (e.g.,
to make dog.toString()
return a string with some information about the dog).
Note
The Java class named Object
provides other methods that are not shown in
Fig. 25. If you are curious, you can have a look at the
Java API documentation for the class named Object
.
Abstract Classes and Methods#
When defining a superclass, we may want to require that each subclass provides some method that is not implemented by the superclass itself. The idea is similar to interfaces, which allow us to specify abstract methods which must be implemented by classes that implement the interface.
We can obtain a similar effect using Java classes. To achieve it, we must use
the abstract
modifier to mark both the class and the method(s) that are
required to be implemented in the subclasses. If we do it, the following rules
apply:
an
abstract
class cannot be instantiated, i.e. we cannot create objects by using the constructor of an abstract class;if a class contains at least one
abstract
method, then the class must be markedabstract
, too;if a subclass extends an
abstract
superclass but does not implement all theabstract
methods required by the superclass, then the subclass must be markedabstract
, too.
This is illustrated in Example 59 below. Then, Example 60 illustrates how abstract classes can be used to revise a previous example (the DTU Christmas Party software).
(Abstract classes and methods)
Consider again the classes Pet
, Dog
, and Cat
in
Example 57, and Snake
in
Example 58. Suppose that the class Pet
has
a new requirement: each object of that class must provide a method getSound()
which returns a string with the sound of the pet. The idea is similar to the
assessment 08 - Pets — but here we have a different
requirement:
in the assessment 08 - Pets, the class
Pet
provides an implementation of the methodgetSound()
that returns"?"
— and that method is overridden by all subclasses, in order to return the appropriate sound;now, we do not want to implement the method
getSound()
in the classPet
— because returning the unspecified sound"?"
for a generic pet does not make much sense. Instead, we want to require all subclasses ofPet
to implement the methodgetSound()
as needed.
We can achieve this by adding to the class Pet
an abstract
method
getSound()
— and in order to do it, we must also mark the whole class Pet
as abstract
. This forces all subclasses of Pet
to implement their own
version of the method getSound()
, as shown below: (the differences with the
classes defined in Example 57 in
Example 58 are highlighted)
1abstract class Pet {
2 private String name;
3 private String species;
4
5 public Pet(String name, String species) {
6 this.name = name;
7 this.species = species;
8 }
9
10 public String getDescription() {
11 return this.name + " (" + this.species + ")";
12 }
13
14 public abstract String getSound();
15}
16
17class Dog extends Pet {
18 public Dog(String name) {
19 super(name, "dog");
20 }
21
22 @Override
23 public String getSound() {
24 return "Woof!";
25 }
26}
27
28class Cat extends Pet {
29 public Cat(String name) {
30 super(name, "cat");
31 }
32
33 @Override
34 public String getSound() {
35 return "Meow!";
36 }
37}
38
39class Snake extends Pet {
40 private boolean isPoisonous;
41
42 public Snake(String name, boolean isPoisonous) {
43 super(name, "snake");
44 this.isPoisonous = isPoisonous;
45 }
46
47 @Override
48 public String getDescription() {
49 var danger = this.isPoisonous ? "DANGER: poisonous" : "non-poisonous";
50 return super.getDescription() + " (" + danger + ")";
51 }
52
53 @Override
54 public String getSound() {
55 return "Hissss!";
56 }
57}
(The @Override
annotations on lines 22, 33, and 53 are not mandatory, but it
is good practice to write them, to spot which methods are required by a
superclass.)
We can now instantiate objects representing the various classes of pets, and see
that each call to getSound()
uses the method of the corresponding class:
var dog = new Dog("Mr Wolf");
var cat = new Cat("Ms Jones");
var snake1 = new Snake("Crawley", false);
var snake2 = new Snake("Mambojumbo", true);
var pets = new Pet[] { cat, dog, snake1, snake2 };
System.out.println("Our pets are:");
for (var p: pets) {
System.out.println(" - " + p.getDescription() + " says: " + p.getSound());
}
and the output of the code snippet above is:
Our pets are:
- Ms Jones (cat) says: Meow!
- Mr Wolf (dog) says: Woof!
- Crawley (snake) (non-poisonous) says: Hissss!
- Mambojumbo (snake) (DANGER: poisonous) says: Hissss!
The UML class diagram of the classes Pet
, Dog
, Cat
, and Snake
is shown
in Fig. 26 below. Notice that the class Pet
is now abstract.
Note that now we cannot directly create objects of the class Pet
any more,
because that class is now abstract
. The only way to create objects of the
abstract class Pet
is to create objects belonging to the (non-abstract
)
subclasses of Pet
.
For example, the following code snippet worked correctly in
Example 56 (because the class Pet
was
not abstract there) — but after marking the class Pet
as abstract
it
does not work any more:
var pet = new Pet("Wilbur", "parrot");
If you try it, Java will report an error like “Pet is abstract; cannot be instantiated”, and your code will not compile.
(The DTU Christmas Party software, using class inheritance)
Let us consider again the
DTU Christmas Party management software. The
code below is the definition of the interface Person
and the classes
Employee
and Guest
previously discussed when we
implemented the DTU Christmas Party software using Java interfaces.
1interface Person {
2 public String getName();
3 public String getInfo();
4}
5
6class Employee implements Person {
7 public String name;
8 public String department;
9
10 public Employee(String name, String department) {
11 this.name = name;
12 this.department = department;
13 }
14
15 public String getName() {
16 return this.name;
17 }
18
19 public String getInfo() {
20 return "Employee, " + this.department;
21 }
22}
23
24class Guest implements Person {
25 public String name;
26 public int ticketNumber;
27
28 public Guest(String name, int ticketNumber) {
29 this.name = name;
30 this.ticketNumber = ticketNumber;
31 }
32
33 public String getName() {
34 return this.name;
35 }
36
37 public String getInfo() {
38 return "Guest, ticket " + this.ticketNumber;
39 }
40}
We can observe that there is some code duplication:
both classes
Employee
andGuest
define a fieldname
of typeString
, andboth classes
Employee
andGuest
define a methodgetName()
.
We can reduce the code duplication in three steps:
we turn
Person
from aninterface
into anabstract class
, and we move duplicated fields and methods code fromEmployee
andGuest
intoPerson
.we declare that the classes
Employee
andGuest
extend the classPerson
(instead of implementing the old interface).
More concretely, when designing the abstract class Person
, we can:
provide an implementation of the method
getName()
reusable by all subclasses;mark the field
name
with theprivate
modifier (since that field is only accessed by the code in the classPerson
); anddeclare an
abstract
methodgetInfo()
, to be implemented by all derived classes.
Then, when implementing the Person
’s subclasses Employee
and Guest
, we
can:
call the
Person
’s constructor (viasuper
) to store the person’s name (that we can retrieve later via the methodgetName()
); andmark the fields
department
(inEmployee
) andticketNumber
(inGuest
) with the modifierprivate
, since they are only accessed internally by their own class.
The result of this revision is shown below: the differences with the code
in Implementing the DTU Christmas Party Software Using interfaces are
highlighted, and we add the recommended annotation @Override
where needed.
1abstract class Person {
2 private String name;
3
4 public Person(String name) {
5 this.name = name;
6 }
7
8 public String getName() {
9 return this.name;
10 }
11
12 public abstract String getInfo();
13}
14
15class Employee extends Person {
16 private String department;
17
18 public Employee(String name, String department) {
19 super(name);
20 this.department = department;
21 }
22
23 @Override
24 public String getInfo() {
25 return "Employee, " + this.department;
26 }
27}
28
29class Guest extends Person {
30 private int ticketNumber;
31
32 public Guest(String name, int ticketNumber) {
33 super(name);
34 this.ticketNumber = ticketNumber;
35 }
36
37 @Override
38 public String getInfo() {
39 return "Guest, ticket " + this.ticketNumber;
40 }
41}
The UML class diagram of the revised classes Person
, Employee
, and Guest
is shown in Fig. 27 below.
Note
After the changes above, the code of the class ChristmasParty
and its
method main(...)
(shown towards the end of
Implementing the DTU Christmas Party Software Using interfaces) can be used
without any change — but a recompilation may be needed to correctly access the
revised classes Person
, Employee
, and Guest
.
Using Interfaces vs. Using Class Inheritance#
Example 60 shows that the situations where we may want to use inheritance may overlap the cases where we may use interfaces. In fact, there are many similarities between the two:
A class can “implement” an interface or “extend” a superclass.
We can define an
abstract
class withabstract
methods, and those methods must be implemented by the subclasses. This is similar to defining an interface with abstract methods that must be implemented by the classes that extend the interface.The fields and methods defined in a superclass can be reused by the subclasses (via inheritance). Instead, interfaces do not allow for this kind of reuse.
Therefore, it may seem that one could always use class inheritance instead of interfaces. However, class inheritance in Java has a key limitation: a class can implement any number of interfaces, but can only extend one superclass. This means that:
class inheritance is strictly hierarchical and tree-based, i.e. superclasses and subclasses form a tree-like hierarchy with just one top-level class;
instead, interfaces can be more flexible, as they can be combined more freely, and not necessarily hierarchically. This is illustrated in Example 61 below.
(Combining class inheritance and interfaces)
Consider Example 53 again, and observe that there is some
code duplication in the classes Scanner
, Printer
, and AllInOne
:
they all have their own fields
model
andserialCode
, andeach class has a copy&pasted implementation of the methods
getModel()
andgetSerialCode()
.
We can reduce this code duplication as follows. (This is similar to what we did in Example 60.)
We turn the interface
Device
into anabstract class
representing a generic device, which “absorbs” all the duplicated code inScanner
,Printer
, andAllInOne
, by defining:the (
private
) fieldsmodel
andserialNumber
a constructor that initialises those fields when a new object is created;
the methods
getModel()
andgetSerialCode()
, which just return the value of the corresponding fields; andthe
abstract
methodgetDescription()
(which must be implemented by the subclasses).
We keep the interfaces
CanScan
andCanPrint
— but they cannot extendDevice
any more (sinceDevice
is now aclass
, not aninterface
); therefore, we add the abstract methodsgetModel()
andgetSerialCode()
to both interfaces, to make sure e.g. that each object which “can scan” also has a model and serial number.Finally, we revise the classes
Scanner
,Printer
, andAllInOne
as follows:we make each class extend the class
Device
, and implement the interfacesCanScan
, orCanPrint
, or both;we change the constructor of each class to make it call the constructor of
Device
(usingsuper
) to store the model and serial code;we delete all the duplicated code already provided by the class
Device
.
The result of this revision is shown below (the differences with
Example 53 are highlighted). You can notice that the
classes Scanner
, Printer
, and AllInOne
are considerably shorter.
1abstract class Device {
2 private String model;
3 private String serialCode;
4
5 public Device(String model, String serialCode) {
6 this.model = model;
7 this.serialCode = serialCode;
8 }
9
10 public String getModel() {
11 return this.model;
12 }
13
14 public String getSerialCode() {
15 return this.serialCode;
16 }
17
18 public abstract String getDescription();
19}
20
21interface CanScan {
22 public String getModel();
23 public String getSerialCode();
24 public int getScannerDPI();
25}
26
27interface CanPrint {
28 public String getModel();
29 public String getSerialCode();
30 public int getPrinterDPI();
31}
32
33class Scanner extends Device implements CanScan {
34 public int dpi;
35
36 public Scanner(String model, int dpi, String serialCode) {
37 super(model, serialCode);
38 this.dpi = dpi;
39 }
40
41 @Override
42 public int getScannerDPI() {
43 return dpi;
44 }
45
46 @Override
47 public String getDescription() {
48 return "Scanner";
49 }
50}
51
52class Printer extends Device implements CanPrint {
53 public int dpi;
54
55 public Printer(String model, int dpi, String serialCode) {
56 super(model, serialCode);
57 this.dpi = dpi;
58 }
59
60 @Override
61 public int getPrinterDPI() {
62 return dpi;
63 }
64
65 @Override
66 public String getDescription() {
67 return "Printer";
68 }
69}
70
71class AllInOne extends Device implements CanScan, CanPrint {
72 public int scannerDpi;
73 public int printerDpi;
74
75 public AllInOne(String model, int scanDpi, int printDpi, String serialCode) {
76 super(model, serialCode);
77 this.scannerDpi = scanDpi;
78 this.printerDpi = printDpi;
79 }
80
81 @Override
82 public int getPrinterDPI() {
83 return this.printerDpi;
84 }
85
86 @Override
87 public int getScannerDPI() {
88 return this.scannerDpi;
89 }
90
91 @Override
92 public String getDescription() {
93 return "Multifunction scanner + printer";
94 }
95}
The updated UML class diagram is shown in Fig. 28 below, where:
each interface and class specifies its methods, with argument types and return types;
the solid arrows show when a class (the origin of the arrow) extends another class (the target of the arrow);
the dashed arrows show when a class (the origin of the arrow) implements one or more interfaces (the target(s) of the arrow).
Note
After the changes above, the code of the class OfficeSupplies
and its method
main(...)
(shown towards the end of Example 53) can be
used without any change — but a recompilation may be needed to correctly
access the revised classes Scanner
, Printer
, and AllInOne
.
Concluding Remarks#
You should now have an understanding of three main topics:
The notions of “abstract class” and “abstract method”;
The similarities and differences between using interfaces and using inheritance;
As previously mentioned, it takes some practice to master the use of inheritance — and, more generally, to design software that satisfies the principles of object-oriented programming. Moreover, there are often multiple ways to design and implement a program that satisfies some given requirements: in fact, in Example 60 and Example 61 we have discussed how to revise previous examples, although we already had working code for them. Software development often proceeds by alternating two phases:
a “top-down” phase, where the design of a program is specified (e.g. using UML diagrams that capture the main classes/interfaces and their relationships) and then implemented in code by following the specification;
a “bottom-up” phase, where the experience gained during implementation may suggest improvements to the overall design (e.g. finding commonalities between different classes, and opportunities for removing code duplication).
The more you program, the more you will gain experience and skills to master all these aspects.
References and Further Readings#
You are invited to read the following sections of the reference book: (Note: they might mention Java features that we will address later in the course)
Section 9.3 - “Class Hierarchies”
Especially “The
Object
Class” and “Abstract Classes”
Exercises#
Note
These exercises are not part of the course assessment. They are provided to help you self-test your understanding.
(From class inheritance to interfaces)
To see why class inheritance can be sometimes more convenient than defining
interface
s, try to rewrite the pet/cat/dog/snake example in
Example 59 by turning Pet
into the
following interface
:
interface Pet {
public String getDescription();
public String getSound();
}
Then adjust the class Dog
, Cat
, and Snake
to make them extend the
interface Pet
above.
Lab and Weekly Assessments#
During the lab you can try the exercises above or begin your work on the weekly assessments below.
Important
For each assessment, you can download the corresponding handout and submit your solution on DTU Autolab: https://autolab.compute.dtu.dk/courses/02312-E24.
For details on how to use Autolab and the assessment handouts, and how to submit your solutions, please read these instructions.
If you have troubles, you can get help from the teacher and TAs.
01 - Object Equality and toString()
#
We have mentioned that the Java root class “Object”
provides default implementations of the methods equals(...)
and .toString()
.
This assessment allows you to experiment with some variations of those methods,
and see why it is sometimes useful to override them.
The task of the assessment is to implement a class that represents a point in 3D
space, with coordinates \(x\), \(y\), and \(z\) (of type double
).
The handout provides a file called Point3D.java
with an “ugly” and simplistic
implementation of a 3D point, called UglyPoint3D
, which includes:
the fields
x
,y
, andz
(of typedouble
) for the point coordinates, andthe following method:
public boolean equals(UglyPoint3D other)
that returns
true
ifthis
point and theother
point are aliases (i.e. ifthis == other
).
Your task is to edit Point3D.java
and implement a series of classes that
progressively improve this simplistic implementation of a 3D point — with
improvements ranging from “meh,” to “ok,” to “good,” to “excellent,” as
explained below.
The class
MehPoint3D
(which must extendUglyPoint3D
) must provide a constructor to create a new 3D point with given coordinates, e.g. as follows:var point = new MehPoint3D(1.0, 2.0, 3.0);
The class
OkPoint3D
(which must extendMehPoint3D
) must provide a constructor similar to the one above. Moreover,OkPoint3D
must override the methodpublic boolean equals(UglyPoint3D other)
: the overridden method must returntrue
ifthis
point has exactly the same coordinates of theother
point.The class
GoodPoint3D
(which must extendOkPoint3D
) must provide a constructor similar to the one above. Moreover,GoodPoint3D
must further override the methodpublic boolean equals(UglyPoint3D other)
: the overridden method must returntrue
if the difference between each coordinate ofthis
point and the corresponding coordinate ofother
point is not greater than 0.0002.Tip
To check the difference between two coordinates, the Java API method Math.abs(…) can be useful.
The class
ExcellentPoint3D
(which must extendGoodPoint3D
) must provide a constructor and a methodequals(...)
similar to the ones above. Moreover, the classExcellentPoint3D
must override the following method (provided by the root classObject
):public String toString()
The overridden method must return a readable representation of the point, with its coordinates. For example, if a point
p
is created as follows:var p = new ExcellentPoint3D(1.0, 2.0, 3.0);
then
p.toString()
must return the string"Point3D(1.0, 2.0, 3.0)"
.
When you are done, submit the modified file Point3D.java
on DTU Autolab.
The handout includes some Java files called Test01.java
, Test02.java
, etc.:
they are test programs that use the code you should write in Point3D.java
,
and they might not compile or work correctly until you complete your work. You
should read those test programs, try to run them, and also run ./grade
to see
their expected outputs — but you must not modify those files.
Warning
The automatic grading on DTU Autolab includes some additional secret checks that test your submission with more points with different coordinates. After you submit, double-check your grading result on DTU Autolab: if the secret checks fail, then your solution is not correct, and you should fix it and resubmit.
02 - Vehicle Fleet Management, Version 3#
This is a follow-up to the assessment
07 - Vehicle Fleet Management, Version 2: the goal is to turn Vehicle
class into
an abstract class
(instead of being a “regular” class
) that contains reusable
methods (and minimises code duplication), similarly to
Example 60 and
Example 61.
You can choose to start this exercise from scratch, or you can edit the file
Vehicle.java
that solves 07 - Vehicle Fleet Management, Version 2.
(either your own file, or the one provided by the teacher as a solution to
07 - Vehicle Fleet Management, Version 2).
Your task is to write in the file Vehicle.java
the classes that correspond to
the class diagram in Fig. 29 below. Note that:
the abstract class
Vehicle
must implement both methodsgetModel()
andgetRegistrationPlate()
, and only haveprivate
fields;the abstract class
Vehicle
must include an abstract methodgetCostPerKm()
which returns adouble
representing estimate of the operating costs per km (in DKK). That abstract method must be implemented by the subclasses ofVehicle
(see details below); andthe classes
Minivan
andTruck
must not reimplement (i.e. override) the methodsgetModel()
andgetRegistrationPlate()
. Instead, the classesMinivan
andTruck
must rely on the methodsgetModel()
andgetRegistrationPlate()
provided by their superclassVehicle
(to minimise code duplication).
The costs per km for the different vehicles are computed as follows:
Minivan: 5 DKK per seat;
Truck: 0.175 DKK per kg of maximum load.
When you are done, submit the modified file Vehicle.java
on DTU Autolab.
The handout includes some Java files called Test01.java
, Test02.java
, etc.:
they are test programs that use the code you should write in Vehicle.java
,
and they might not compile or work correctly until you complete your work. You
should read those test programs, try to run them, and also run ./grade
to see
their expected outputs — but you must not modify those files.
Hint
You may want to store the model and registration plate in private
fields
of the class Vehicle
, that you can initialise using Vehicle
’s constructor
(which you can call using super
,
as shown in Calling a “super” Constructor).
03 - Electronic Devices Shop, Version 2#
This is a follow-up to the assessment
05 - Electronic Devices Shop, and the starting point is its
solution, i.e. the file Device.java
(you can use either your own file, or the
one provided by the teacher as a solution to
05 - Electronic Devices Shop). The goal is to turn Device
into
an abstract class
(instead of being an interface
) to minimise code
duplication.
Your task is to write in the file Device.java
the classes and interfaces that
correspond to the class diagram in Fig. 30 below.
Note that:
the interfaces
CanTakePictures
,CanMakeCalls
, andCanPlayMusic
are now independent from each other, and now they all include the abstract methodgetModel()
;the abstract class
Device
must implement the methodgetModel()
, andthe other derived classes must not reimplement (i.e. override) the method
getModel()
: they must instead rely on the methodgetModel()
provided by their superclassDevice
(to minimise code duplication).
When you are done, submit the modified file Device.java
on DTU Autolab.
The handout includes some Java files called ClassTestUtils.java
and
Test01.java
, Test02.java
, etc.: they are test programs that use the code you
should write in Device.java
, and they might not compile or work correctly
until you complete your work. You should read those test programs, try to run
them, and also run ./grade
to see their expected outputs — but you must
not modify those files.
Hint
You may want to store the model in a private
field
of the class Device
, that you can initialise using Device
’s constructor
(which you can call using super
,
as shown in Calling a “super” Constructor).
04 - Arithmetic Expressions, Version 2#
This is a follow-up to the assessment 03 - Arithmetic Expressions, and the
starting point is its solution, i.e. the file Expression.java
(you can use
either your own file, or the one provided by the teacher as a solution to
03 - Arithmetic Expressions).
The solution to 03 - Arithmetic Expressions shows some duplicated code: all
classes that implement binary operations (i.e. Addition
, Subtraction
,
Multiplication
, and Division
) have very similar fields for storing their
sub-expressions, and very similar code e.g. for the method format()
.
Your task is to improve the solution to 03 - Arithmetic Expressions: edit the
file Expression.java
and introduce an abstract class called BinaryOperation
that implements the interface Expression
, and is a superclass for Addition
,
Subtraction
, Multiplication
, and Division
. You should try to minimise the
duplicated code (by moving methods and fields into the class BinaryOperation
)
as much as you can, similarly to Example 60
and Example 61.
The UML diagram of the revised classes should look as in Fig. 31 below.
When you are done, submit the modified file Expression.java
on DTU Autolab.
The handout includes some Java files called Test01.java
, Test02.java
, etc.:
they are test programs that use the code you should write in Expression.java
,
and they might not compile or work correctly until you complete your work. You
should read those test programs, try to run them, and also run ./grade
to see
their expected outputs — but you must not modify those files.
Note
This assessment has several potential solutions. In fact, the UML diagram in
Fig. 31 omits various details in the class
BinaryOperation
— e.g. what is its constructor (if any), what methods it
implements, and whether it has non-private
fields: you are free to choose
those details as you like, trying to minimise code duplication as much as
possible. After the submission deadline expires, please compare your solution
with the one provided by the teacher.
Warning
The automatic grading on DTU Autolab includes some additional secret checks that test your submission with more expressions. After you submit, double-check your grading result on DTU Autolab: if the secret checks fail, then your solution is not correct, and you should fix it and resubmit.