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.

Example 54 (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 of Pet is also a field/method of Dog. In fact, you can notice that the constructor of Dog initialises the fields this.name and this.species (lines 3–4), and those fields are defined in Pet. Moreover,

  • each object of the class Dog is also an object of the class Pet.

(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).

UML class diagram for the pet classes

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

Example 55 (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 interfaces (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 Pets (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.

Example 56 (Using 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)

Example 57 (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.

Example 58 (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 superclass Pet, using super 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 Pets 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:

  • 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 generic Devices, and the actual method being executed may vary (since the method may be provided by an object of the class Scanner, Printer, or AllInOne);

    • similarly, in Example 58, you can observe a loop that calls the method .getDescription() on each object contained in an array of generic Pets, and the actual method being executed may vary (since the method may be provided by the class Pet or Snake).

    (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:

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

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 a public constructor that takes the machines’s location (as a String) and the number of coffee and chocolate servings it holds (as integers). 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 methods serveCoffee(), serveChocolate(), and serveWienerMelange(). For instance, if the advanced vending machine m holds 2 servings of coffee and 30 units of cash, then the method call m.serveCoffees(5) must return 2, because the vending machine runs out of coffee after providing 2 servings; after the method call, the vending machine m 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 class VendingMachine.

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

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

Fig. 22 UML diagram for the revised classes in 07 - Vehicle Fleet Management, Version 2.#

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.

UML class diagram for the pet classes in the assignment "08 - Pets"

Fig. 23 UML class diagram showing the relationship between the classes Pet, Dog, Cat, Frog, and their subclasses.#

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 method getDescription(), and that method must be inherited and reused by all subclasses (i.e., the method must not be overridden by any subclass of Pet). The method getDescription() must return a string describing the name and species of the pet. E.g., if a Pet object p has name "Pernille" and species "capibara", then p.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 of Dog. For example, suppose we have an object of the class Chihuahua 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 of Cat.

    • The species of an object of the class Frog is "frog" — and this also applies to objects of all subclasses of Frog.

  • 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 method getSound() of its superclass.)

    Table 9 Animal sounds#

    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 class Pet, that you can initialise using Pet’s constructor (which you can call from the subclasses using super, 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…