Module 9, Part 2: Class Inheritance and Principles of Object-Oriented Programming#
In this second part of Module 9 we explore how we can extend classes using inheritance to improve the structure and reusability of our Java code. We then discuss the main principles of object-oriented programming.
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.
Class Inheritance: Defining a Class by Extending Another Class#
In this section explore inheritance in Java. The basic idea is quite simple (i.e. define a class by extending another class) — but this simple idea comes with many nuanced details, and it has important ramifications in the way we can design our Java programs, and how the programs behave when executed. In the next subsections we address the following topics.
Fundamentals#
In Java, we can specify that a class extends another class:
the class that performs the extension is called subclass or derived class;
the class that is extended is called superclass or parent class
The subclass inherits all the fields and methods of the superclass: the idea
is that inherited fields and methods are available to the code written in the
subclass, and they can be accessed and called through the object of the
subclass — as if they were defined in the subclass itself. The only exception
are the fields and methods that are declared private
in the superclass: they
are not accessible outside the superclass.
Inheritance allows us to maximise code reuse between the superclass and the subclass(es); moreover, each object of the subclass is also an object of the superclass, which further increases code flexibility and reusability. This is illustrated in Example 54 and Example 55 below.
Important
A subclass does not inherit the constructor(s) of its superclass. We will discuss this shortly, when introducing “super” constructor calls.
Some key differences between class inheritance and using Java interfaces:
A class can implement multiple interfaces, but it can only extend one other class.
An interface cannot declare object fields, whereas a class can declare both fields and methods — and if they can be inherited by the subclasses.
(Basic example of class inheritance: pets, cats, and dogs)
Suppose we need to implement a program that handles pets of various kinds, e.g.
cats and dogs. Each pet has a name and a species, and must provide a
description. We can approach the task by defining a generic class Pet
that
captures all the common features of the various kinds of pets — i.e. the class
Pet
has fields for the name and species, and a method for returning a
description, as follows:
1class Pet {
2 public String name;
3 public String species;
4
5 public String getDescription() {
6 return this.name + " (" + this.species + ")";
7 }
8}
Then we can define the sub-classes (a.k.a. derived classes) Dog
and
Cat
, that extend Pet
:
1class Dog extends Pet {
2 public Dog(String name) {
3 this.name = name;
4 this.species = "dog";
5 }
6}
7
8class Cat extends Pet {
9 public Cat(String name) {
10 this.name = name;
11 this.species = "cat";
12 }
13}
By writing e.b. class Dog extends Pet ...
, we are declaring two things:
each (non-
private
) field and method ofPet
is also a field/method ofDog
. In fact, you can notice that the constructor ofDog
initialises the fieldsthis.name
andthis.species
(lines 3–4), and those fields are defined inPet
. Moreover,each object of the class
Dog
is also an object of the classPet
.
(Similar considerations apply to the class Cat
, since it also extends Pet
).
The UML class diagram of the classes Pet
, Dog
, and Cat
is shown
in Fig. 21 below, where:
each class specifies its
public
fields and methods (the latter 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).
(Objects of the subclass are also objects of the superclass)
Consider the classes Pet
, Dog
, and Cat
defined in
Example 54. Since each object of the classes
Dog
and Cat
is also an object of the class Pet
, we get a flexibility that
reminds us of what we achieved using interface
s (as shown in
Implementing the DTU Christmas Party Software Using interfaces).
For instance, we can create objects of the classes Dog
and Cat
, and call the
method getDescription()
provided by their superclass Pet:
var dog = new Dog("Mr Wolf");
var cat = new Cat("Ms Jones");
System.out.println("Dog's description: " + dog.getDescription());
System.out.println("Cat's description: " + cat.getDescription());
and the output of the code snippet above is:
Dog's description: Mr Wolf (dog)
Cat's description: Ms Jones (cat)
We can also put the dog
and cat
objects above in a generic array of Pet
s
(since each object of the classes Dog
and Cat
also belongs to the superclass
Pet
):
var pets = new Pet[] {cat, dog};
And as expected, each object in the array pets
lets us use the programming
interface (i.e. the public
methods and fields) provided in the base class
Pet
, e.g.:
System.out.println("Our pets are:");
for (var p: pets) {
System.out.println(" - " + p.getDescription());
}
which produces the output:
Our pets are:
- Ms Jones (cat)
- Mr Wolf (dog)
Calling a “super
” Constructor#
When a subclass extends a superclass, the constructors of the superclass are
not inherited by the subclass. Still, when writing the subclass, it is
sometimes handy to call a constructor of the superclass — for instance,
to reduce code duplication, and to initialise fields that are declared private
in the superclass. This is illustrated in
Example 56 and
Example 57 below.
super
to call the superclass constructor)
Consider the classes Dog
and Cat
in
Example 54: they have some duplicated code in
their constructors, which are very similar. We can remove the duplication by
adding a generic constructor to the class Pet
that takes as arguments the
pet’s name
and species
, and initialises the corresponding fields: (the
differences with Example 54 are highlighted)
1class Pet {
2 public String name;
3 public 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}
We can then revise the classes Dog
and Cat
and make their constructors
simply call the constructor of their superclass Pet
, passing the appropriate
arguments: (the differences with Example 54 are
highlighted)
1class Dog extends Pet {
2 public Dog(String name) {
3 super(name, "dog");
4 }
5}
6
7class Cat extends Pet {
8 public Cat(String name) {
9 super(name, "cat");
10 }
11}
After this change, the code duplication in Dog
and Cat
has been reduced, and
the code snippets shown in
Example 55 still work.
(Try them!)
We can now also define objects of the class Pet
, by using its constructor
directly:
var pet = new Pet("Wilbur", "parrot");
System.out.println("Pet's description: " + pet.getDescription());
and the code snippet above displays the output:
Pet's description: Wilbur (parrot)
(Making class fields private (when possible))
After the changes described in Example 56,
we can make a further observation: the classes Dog
and Cat
are not accessing
the fields name
and species
of the class Pet
any more (because the call to
the super
constructor initialises them as needed). Therefore, we can make
the fields name
and species
both private
in the class Pet
, without
changing the subclasses Dog
and Cat
, as follows: (the differences with the
class Pet
in Example 56 are highlighted)
1class 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}
With this change, we have reduced the programming interface of the class Pet
(as it now only consists of the constructor and the method getDescription()
),
and the code snippets shown in
Example 55 still work. (Try
them!)
Also, you will notice that if you modify the classes Dog
or Cat
and add a
method that tries to access the private
fields this.name
or
this.description
, you will get an error, because those fields are not visible
in the subclass. (Try it!)
Note
Making class fields and methods private
whenever possible (as shown at the end
of Example 56) is generally good practice:
this minimises the programming interfaces of classes and objects, and makes
software easier to maintain. This is because as software evolves, changing a
private
field or method will impact the owner class, but will not require
modifying other parts of our programs.
Overriding Methods#
It is sometimes handy for a subclass to override (i.e., redefine) a method that is already provided by a superclass, to make it “more specialised” for the subclass. When this happens, the method in the subclass has “priority” over the original one — as shown in Example 58 below.
(Overriding a method in a subclass)
Consider again the class Pet
in
Example 57, and suppose that we need
to define a derived class Snake
to represent a pet snake, with information on
whether the snake is poisonous or not. Also, suppose we want the method
getDescription()
of Snake
objects to report whether the snake is poisonous
or not.
This last requirement means that we cannot reuse the method getDescription()
provided by the class Pet
. However, we can override that method in the
class Snake
, as follows:
1class Snake extends Pet {
2 private boolean isPoisonous;
3
4 public Snake(String name, boolean isPoisonous) {
5 super(name, "snake");
6 this.isPoisonous = isPoisonous;
7 }
8
9 @Override
10 public String getDescription() {
11 var danger = this.isPoisonous ? "DANGER: poisonous" : "non-poisonous";
12 return super.getDescription() + " (" + danger + ")";
13 }
14}
Note
On line 12, we directly call the method
getDescription()
provided by the superclassPet
, usingsuper
to access it.The
@Override
annotation on line 9 is not mandatory, but it is good practice to write it, because it lets us spot at a glance whether a method overrides another method in a superclass.
We can now create objects of the class Snake
, and we can observe that if we
call their method getDescription()
, the “more specialised” version defined in
Snake
is executed:
var snake1 = new Snake("Crawley", false);
var snake2 = new Snake("Mambojumbo", true);
System.out.println("First snake description: " + snake1.getDescription());
System.out.println("Second snake description: " + snake2.getDescription());
and the output of the code snippet above is:
First snake description: Crawley (snake) (non-poisonous)
Second snake description: Mambojumbo (snake) (DANGER: poisonous)
We can also observe the “more specialised” method getDescription()
being used,
e.g., if we create an array of Pet
s including snake1
and snake2
defined
above, and then call getDescription()
on each object in the array.
var dog = new Dog("Mr Wolf");
var cat = new Cat("Ms Jones");
var pets = new Pet[] {cat, dog, snake1, snake2};
System.out.println("Our pets are:");
for (var p: pets) {
System.out.println(" - " + p.getDescription());
}
The code above will print on the console:
Our pets are:
- Ms Jones (cat)
- Mr Wolf (dog)
- Crawley (snake) (non-poisonous)
- Mambojumbo (snake) (DANGER: poisonous)
Observe that the code above calls p.getDescription()
using a loop variable p
of type Pet
— and when p
refers to an object of the subclass Snake
, then
the “more specialised” method getDescription()
provided by the class Snake
is executed.
What is “Object-Oriented Programming”?#
You may find various (more or less similar) definitions of “object-oriented programming” — so, the definition below tries to be a general enough, without delving into excessive details.
Object-oriented programming is a programming approach where programs are designed and developed using objects as the main building block; objects provide fields (a.k.a. attributes) to store data, and methods to read and manipulate that data; the shape of each object (i.e. which fields and methods it provides) is determined by the class of the object.
We have already seen and used all the elements above — but the object-oriented approach also encourages the use of the following code design techniques.
Leverage encapsulation, i.e. minimise the programming interface of objects by making their fields and methods
private
(whenever possible).Leverage abstraction, i.e. design code that targets generic (abstract) programming interfaces. We have already done this, by:
defining interfaces and writing code that only depends on them, and
writing code that that only depends on a superclass (not on its specific subclasses), like the loops on arrays of generic
Pet
s in Example 55 and Example 58.
Leverage inheritance, i.e. reuse parts of a superclass in one or more subclasses, as described above.
Leverage subtype polymorphism, i.e. design generic code that manipulates objects without depending on their specific classes — and yet, let the code be “polymorphic” (i.e. have different behaviours) depending on the actual object being manipulated. We have already seen subtype polymorphism in action, e.g.:
in Example 53, you can observe that our code calls the method
.getDescription()
on each object contained in an array of genericDevice
s, and the actual method being executed may vary (since the method may be provided by an object of the classScanner
,Printer
, orAllInOne
);similarly, in Example 58, you can observe a loop that calls the method
.getDescription()
on each object contained in an array of genericPet
s, and the actual method being executed may vary (since the method may be provided by the classPet
orSnake
).
(We will reprise the topic of subtype polymorphism in more detail later in the course.)
Concluding Remarks#
You should now have an understanding of two main topics:
What is class inheritance and how to use it, to improve the structure and the code reuse in our programs
What is object-oriented programming and what are its main principles.
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.
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.1 - “Creating Subclasses”
Section 9.2 - “Overriding Methods”
Section 9.3 - “Class Hierarchies”
Section 9.4 - “Visibility”
Lab and Weekly Assessments#
During the lab you can 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.
06 - Vending Machines, Version 2#
This is a follow-up to the assessment
03 - Vending Machines. Your task is to edit
the file VendingMachine.java
(which you can take from the solution to
03 - Vending Machines) and implement a
new class called AdvancedVendingMachine
that extends the class
VendingMachine
.
The new class AdvancedVendingMachine
can only have private
fields, and it
must inherit all the methods of VendingMachine
. In addition, objects of the
class AdvancedVendingMachine
must satisfy the following requirements.
The class
AdvancedVendingMachine
must have apublic
constructor that takes the machines’s location (as aString
) and the number of coffee and chocolate servings it holds (asint
egers). A newly-created vending machine must hold no cash. For instance, the constructor should allow us to create a vending machine object with 10 servings of coffee and 12 servings of chocolate, as follows:var m = new AdvancedVendingMachine("DTU Building 324", 10, 12);
AdvancedVendingMachine
objects must have the following method:public int serveCoffees(int n)
public int serveChocolates(int n)
public int serveWienerMelanges(int n)
These methods attempt to provide
n
servings of the requested beverage, and return the number of servings are actually provided. These methods must follow the same principles of the inherited methodsserveCoffee()
,serveChocolate()
, andserveWienerMelange()
. For instance, if the advanced vending machinem
holds 2 servings of coffee and 30 units of cash, then the method callm.serveCoffees(5)
must return 2, because the vending machine runs out of coffee after providing 2 servings; after the method call, the vending machinem
must have 10 units of cash left.
When you are done, submit the modified file VendingMachine.java
on DTU Autolab.
Tip
The constructor of
AdvancedVendingMachine
can be easily implemented by calling the “super” constructor…The 3 methods of
AdvancedVendingMachine
can be implemented by reusing the methods of the parent classVendingMachine
.This assignment can be solved in various ways — but there is a simple solution where the class
AdvancedVendingMachine
does not have any field.
07 - Vehicle Fleet Management, Version 2#
This is a follow-up to the assessment
04 - Vehicle Fleet Management: the goal is to turn Vehicle
into
a class
(instead of being an interface
) that contains reusable
methods (and minimises code duplication).
You can choose to start this exercise from scratch, or you can edit the file
Vehicle.java
that solves 04 - Vehicle Fleet Management (either
your own file, or the one provided by the teacher as a solution to
04 - Vehicle Fleet Management).
Your task is to write in the file Vehicle.java
the classes that correspond to
the class diagram in Fig. 22 below. Note that:
the class
Vehicle
must implement both methodsgetModel()
andgetRegistrationPlate()
, and only haveprivate
fields; 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).
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 from the subclasses using super
,
as shown in Calling a “super” Constructor).
08 - Pets#
This assignment builds upon Example 54 and
Example 56, and you can reuse and adapt some
code from there. Your task is to edit the file Pet.java
provided in the
handout and implement the class hierarchy shown in
Fig. 23 below.
Here are the requirements for the Pet
class hierarchy. (Observe that, compared
to Example 54 and
Example 56, there are several differences.)
The class
Pet
has a constructor that takes as arguments the name and species of the pet.All the fields of all classes must be
private
(correspondingly, the UML diagram in Fig. 23 does not show any field).The class
Pet
must implement the methodgetDescription()
, and that method must be inherited and reused by all subclasses (i.e., the method must not be overridden by any subclass ofPet
). The methodgetDescription()
must return a string describing the name and species of the pet. E.g., if aPet
objectp
has name"Pernille"
and species"capibara"
, thenp.getDescription()
must return the string:Pernille (capibara)
The classes that directly inherit from
Pet
also determine the species of the resulting object. More specifically:The species of an object of the class
Dog
is"dog"
— and this also applies to objects of all subclasses ofDog
. For example, suppose we have an object of the classChihuahua
created as follows:var c = new Chihuahua("Prosit")
Then,
c.getDescription()
must return the string:Prosit (dog)`
The species of an object of the class
Cat
is"cat"
— and this also applies to objects of all subclasses ofCat
.The species of an object of the class
Frog
is"frog"
— and this also applies to objects of all subclasses ofFrog
.
Each class must implement its own version of the method
getSound()
, which must return a string according to the following table. (In other words, each class must override the methodgetSound()
of its superclass.)# Pet class
Sound
Pet
"?"
Dog
"Woof!"
Greyhound
"WOOF!!!"
Chihuahua
"Arf!"
Cat
"Meow!"
NorwegianForestCat
"MEOOOW!!!"
Frog
"Ribbit!"
HornedFrog
"Croak!"
When you are done, submit the modified file Pet.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 Pet.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 name and species of a pet in
private
fields of the classPet
, that you can initialise usingPet
’s constructor (which you can call from the subclasses usingsuper
, as shown in Calling a “super” Constructor).In the constructors of the classes in the lower row of Fig. 23, you may want to call the
super
constructor to initialise the pet’s name and species…