Module 10, part 1: More on Class Inheritance and abstract classes#

In this first part of Module 10 we explore three topics related to class 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).

UML class diagram for the pet classes

Fig. 24 UML class diagram showing the relationship between the classes Pet, Dog, and Cat. (This diagram was previously shown in Fig. 21.)#

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.

UML class diagram for the pet classes, including the root class Object

Fig. 25 UML class diagram showing the relationship between the classes Pet, Dog, Cat, and Snake — including the root class named Object provided by Java.#

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 marked abstract, too;

  • if a subclass extends an abstract superclass but does not implement all the abstract methods required by the superclass, then the subclass must be marked abstract, 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).

Example 59 (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 method getSound() 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 class Pet — because returning the unspecified sound "?" for a generic pet does not make much sense. Instead, we want to require all subclasses of Pet to implement the method getSound() 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.

UML class diagram for the pet classes

Fig. 26 UML class diagram showing the relationship between the classes Pet, Dog, Cat, and Snake.#

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.

Example 60 (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 and Guest define a field name of type String, and

  • both classes Employee and Guest define a method getName().

We can reduce the code duplication in three steps:

  1. we turn Person from an interface into an abstract class, and we move duplicated fields and methods code from Employee and Guest into Person.

  2. we declare that the classes Employee and Guest extend the class Person (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 the private modifier (since that field is only accessed by the code in the class Person); and

  • declare an abstract method getInfo(), to be implemented by all derived classes.

Then, when implementing the Person’s subclasses Employee and Guest, we can:

  • call the Person’s constructor (via super) to store the person’s name (that we can retrieve later via the method getName()); and

  • mark the fields department (in Employee) and ticketNumber (in Guest) with the modifier private, 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.

UML class diagram for DTU Christmas Party, using inheritance

Fig. 27 UML class diagram showing the relationship between the classes Person, Employee, and Guest.#

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 with abstract 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.

Example 61 (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 and serialCode, and

  • each class has a copy&pasted implementation of the methods getModel() and getSerialCode().

We can reduce this code duplication as follows. (This is similar to what we did in Example 60.)

  1. We turn the interface Device into an abstract class representing a generic device, which “absorbs” all the duplicated code in Scanner, Printer, and AllInOne, by defining:

    • the (private) fields model and serialNumber

    • a constructor that initialises those fields when a new object is created;

    • the methods getModel() and getSerialCode(), which just return the value of the corresponding fields; and

    • the abstract method getDescription() (which must be implemented by the subclasses).

  2. We keep the interfaces CanScan and CanPrint — but they cannot extend Device any more (since Device is now a class, not an interface); therefore, we add the abstract methods getModel() and getSerialCode() to both interfaces, to make sure e.g. that each object which “can scan” also has a model and serial number.

  3. Finally, we revise the classes Scanner, Printer, and AllInOne as follows:

    • we make each class extend the class Device, and implement the interfaces CanScan, or CanPrint, or both;

    • we change the constructor of each class to make it call the constructor of Device (using super) 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).

UML class diagram for the office supplies shop software, using interfaces and inheritance

Fig. 28 UML class diagram showing the relationship between the interfaces and classes of the “office supplies shop” example, revised using class inheritance. Observe that the Device is the superclass of all device subclasses, the interfaces CanScan and CanPrint are now completely independent, and the class AllInOne implements both interfaces.#

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:

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.

Exercise 31 (From class inheritance to interfaces)

To see why class inheritance can be sometimes more convenient than defining interfaces, 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

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, and z (of type double) for the point coordinates, and

  • the following method:

    public boolean equals(UglyPoint3D other)
    

    that returns true if this point and the other point are aliases (i.e. if this == 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 extend UglyPoint3D) 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 extend MehPoint3D) must provide a constructor similar to the one above. Moreover, OkPoint3D must override the method public boolean equals(UglyPoint3D other): the overridden method must return true if this point has exactly the same coordinates of the other point.

  • The class GoodPoint3D (which must extend OkPoint3D) must provide a constructor similar to the one above. Moreover, GoodPoint3D must further override the method public boolean equals(UglyPoint3D other): the overridden method must return true if the difference between each coordinate of this point and the corresponding coordinate of other 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 extend GoodPoint3D) must provide a constructor and a method equals(...) similar to the ones above. Moreover, the class ExcellentPoint3D must override the following method (provided by the root class Object):

    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 methods getModel() and getRegistrationPlate(), and only have private fields;

  • the abstract class Vehicle must include an abstract method getCostPerKm() which returns a double representing estimate of the operating costs per km (in DKK). That abstract method must be implemented by the subclasses of Vehicle (see details below); and

  • the classes Minivan and Truck must not reimplement (i.e. override) the methods getModel() and getRegistrationPlate(). Instead, the classes Minivan and Truck must rely on the methods getModel() and getRegistrationPlate() provided by their superclass Vehicle (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.

UML class diagram for "Vehicle fleet management, version 3" assessment

Fig. 29 UML diagram for the revised classes in 02 - Vehicle Fleet Management, Version 3.#

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, and CanPlayMusic are now independent from each other, and now they all include the abstract method getModel();

  • the abstract class Device must implement the method getModel(), and

  • the other derived classes must not reimplement (i.e. override) the method getModel(): they must instead rely on the method getModel() provided by their superclass Device (to minimise code duplication).

UML class diagram for "Electronic devices shop, version 2" assessment

Fig. 30 UML diagram for the revised classes and interfaces in 03 - Electronic Devices Shop, Version 2.#

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.

UML class diagram for "Arithmetic expressions, version 2" assessment

Fig. 31 UML diagram for the revised classes in 04 - Arithmetic Expressions, Version 2.#

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.