Skip to content

Programming by Design

If you're not prepared to be wrong, you'll never come up with anything original. – Sir Ken Robinson

  • About
  • Java-PbD
  • C-PbD
  • ASM-PbD
  • Algorithms
  • Other

Chapter 11 – Inheritance and Polymorphism

Posted on June 2, 2019January 13, 2025 By William Jojo
Java Book

Updated January 13, 2025

Table of contents

    Inheritance
    The Object Class
    Abstract Methods and Classes
    Interfaces
    Inheritance vs. Composition
    Exercises

Inheritance

Recall from our discussion on creating a GUI that we chose to use the phrase extends Application (for JavaFX). This meant the class containing our program could obtain the ability to display a window and place text fields and buttons within the viewable area. The discussion then briefly described the benefits of extending the class in that our program was now part of that class.

Of course, we did not add anything new to the existing JFrame or Application class, or what we call the superclass (also known as the base class). Our program became the subclass (or derived class) by way of inheritance.

The subclass inherits all of the properties of the superclass. As a result, we can create instance variables of both the superclass and the subclass types in the same object. This also reduces programming complexity, as any change to the superclass design is automatically inherited by the subclass the next time we compile our program.

The details of inheritance are relatively simple. Single inheritance is a subclass that inherits a single superclass’s properties. The concept of multiple inheritance is a subclass that inherits the properties of more than one superclass. The single and multiple inheritance principles are created to describe how a subclass can come into being, but Java only supports single inheritance. In other words, we can only extend one class at a time. With inheritance, there are some strict rules which are:

  • The private members of the superclass cannot be accessed by the subclass.
  • The public member of the superclass are directly accessible to the subclass.
  • The subclass can contain additional data or method members.
  • All data members of the superclass are also data members of the subclass (because of inheritance).
  • The subclass can overload the public methods of the superclass.
  • The subclass can redefine or override public methods of the superclass.

We will use the code in Example 1, Example 2 to demonstrate our itemized list of limitations and capabilities.

Car.java
public class Car {

    private String color;
    private int doors;
    private boolean hatchback;
    protected String make;

    /**
     * One constructor to specify certain features.
     *
     * @param make      String value for make
     * @param doors     an int number of doors
     * @param hatchback indicating if it's a hatchback
     */
    public Car(String make, int doors, boolean hatchback) {
        this.make = make;
        this.doors = doors;
        this.hatchback = hatchback;
    }

    public int getDoors() {
        return doors;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String c) {
        color = c;
    }

    public boolean getHatchback() {
        return hatchback;
    }

    public String getMake() {
        return make;
    }

    @Override
    public String toString() {
        String hb = hatchback ? " with hatchback." : ".";
        return make + " " + doors + " door" + hb;
    }
}

Example 1: Source code for the class Car.

The code for Car in Example 1 has three private instance variables color and doors and hatchback. There is one constructor, one mutator method, and four accessor methods. Included is a toString() method for details of the instance variable values. This class is basic and conforms to all the user-defined class components we have seen previously.

Remember that toString() is overriding the inherited one from Object. We saw this with user-defined classes in Chapter 8.

We are also introducing the use of protected here. The subclass would not have direct access if it were marked private. By marking it protected, it remains private to the world but accessible to subclasses.

Honda.java
public class Honda extends Car {

    public enum pkg_type {LX, EX, SE, EX_L, SPORT, TOURING}

    public enum eng_type {InLine4Cylinder, V6}

    public enum model_type {Accord, Civic, Fit, CR_V, Odyssey}

    private pkg_type pkg;
    private eng_type engine;
    private model_type model;
    private int year;

    /**
     * One constrcutor for Hondas.
     *
     * @param color     String value for color
     * @param doors     number of doors
     * @param hatchback do we have a hatchback?
     * @param model     enum for the model
     */
    public Honda(String color, int doors, boolean hatchback, model_type model) {
        // Car wants make, doors and hatchback - in that order!
        super("Honda", doors, hatchback);

        this.model = model;
        //this.color = color; // NO NO NO!
        setColor(color);
    }

    public model_type getModel() {
        return model;
    }

    public void setEngine(eng_type engine) {
        this.engine = engine;
    }

    public eng_type getEngine() {
        return engine;
    }

    public void setPkg(pkg_type p) {
        pkg = p;
    }

    @Override
    public String toString() {
        String hb = hatchback ? " with hatchback." : ".";
        return make + " " + model + " " + pkg + " " + getEngine() + " " + getDoors() + " door" + hb;
    }
}

Example 2: Source code for Honda which extends Car.

In Example 2, Honda is defined as a class that extends Car. By doing so, we are indicating that Honda is to inherit all of the properties of Car. In addition to this inheritance, we are defining four additional private instance variables pkg, engine, model and year.

Honda has three enum types, one constructor, two accessors, and two mutator methods. The method toString() has, again, been overridden for the subclass.

Our subclass constructor invokes the superclass constructor by calling super() with an appropriate number of arguments. Whenever we call the superclass constructors, they must be the first line of the subclass constructor body and must use the super reserved word.

We can also use the super reserved word to access methods within the superclass. This is especially necessary when our subclass has methods that override the ones of the superclass. These forms are shown in the setString() and toString() methods.

It is important to note that we are calling the mutator methods of the superclass from within our subclass and not attempting to modify the instance variables of the superclass. This direct manipulation would not be possible since the superclass instance variables are private and, therefore, cannot be accessed from the subclass.

If, however, we did want a subclass to have direct access to an instance variable of the superclass while still disallowing direct access outside the class, we could use a different modifier called protected. A protected instance variable of the superclass can be directly accessed from a subclass and still requires all other access outside the class to be done using the accessor and mutator methods. So if we wanted the color instance variable of Car to be accessed from Honda, we could simply modify its declaration as follows:

protected String color;

No matter how many generations of class are derived from a superclass, all of the public and protected members of the superclass will continue to be accessible to all the subsequent generations. This is also true for any intermediate generation forward in the inheritance hierarchy.

We must be sure that all subclasses make appropriate calls to the super() constructors and that the superclass initializes those instance variables correctly.

Technical Note
In the next example, the vendor cannot yet support separate files for the additional classes. For the sake of demonstration, Car and Honda have been added to the exerciser program.
CarExerciser
class Car {

    private String color;
    private int doors;
    private boolean hatchback;
    protected String make;

    /**
     * One constructor to specify certain features.
     *
     * @param make      String value for make
     * @param doors     an int number of doors
     * @param hatchback indicating if it's a hatchback
     */
    public Car(String make, int doors, boolean hatchback) {
        this.make = make;
        this.doors = doors;
        this.hatchback = hatchback;
    }

    public int getDoors() {
        return doors;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String c) {
        color = c;
    }

    public boolean getHatchback() {
        return hatchback;
    }

    public String getMake() {
        return make;
    }

    @Override
    public String toString() {
        String hb = hatchback ? " with hatchback." : ".";
        return make + " " + doors + " door" + hb;
    }
}

class Honda extends Car {

    public enum pkg_type {LX, EX, SE, EX_L, SPORT, TOURING}

    public enum eng_type {InLine4Cylinder, V6}

    public enum model_type {Accord, Civic, Fit, CR_V, Odyssey}

    private pkg_type pkg;
    private eng_type engine;
    private model_type model;
    private int year;

    /**
     * One constrcutor for Hondas.
     *
     * @param color     String value for color
     * @param doors     number of doors
     * @param hatchback do we have a hatchback?
     * @param model     enum for the model
     */
    public Honda(String color, int doors, boolean hatchback, model_type model) {
        // Car wants make, doors and hatchback - in that order!
        super("Honda", doors, hatchback);

        this.model = model;
        //this.color = color; // NO NO NO!
        setColor(color);
    }

    public model_type getModel() {
        return model;
    }

    public void setEngine(eng_type engine) {
        this.engine = engine;
    }

    public eng_type getEngine() {
        return engine;
    }

    public void setPkg(pkg_type p) {
        pkg = p;
    }

    @Override
    public String toString() {
        String hb = getHatchback() ? " with hatchback." : ".";
        return make + " " + model + " " + pkg + " " + getEngine() + " " + getDoors() + " door" + hb;
    }
}

public class CarExerciser {

    public static void main(String[] args) {

        Car somecar = new Car("Acura", 4, true), othercar;
        Honda hondacar = new Honda("Silver", 4, true, Honda.model_type.Fit);

        hondacar.setPkg(Honda.pkg_type.SPORT);
        hondacar.setEngine(Honda.eng_type.InLine4Cylinder);

        System.out.println(somecar);
        System.out.println(hondacar);

        // same types
        othercar = somecar;
        System.out.println(othercar);

        // polymorphism (Car references Honda)
        othercar = hondacar;
        // Late-binding
        System.out.println(othercar);
    }
}

Example 3: CarExerciser program to test Car and Honda objects.

Let’s look at our exerciser program in Example 3. We have declared reference variable somecar of class type Car and reference variable hondacar of class type Honda.

First, let us look at the very commonplace appearance of somecar and hondacar. They are populated with objects from their respective constructors and then made part of a call to println(). This implies a call to the toString() methods for each object. Recall that Honda redefined (override) toString() to include the additional instance variables.

We then call some subclass mutator methods to alter the values of the instance variables for hondacar. Now for something exciting. othercar is assigned the value of somecar, then we print othercar which calls the toString() method of Car which is exactly as we would expect to happen.

However, we then assign hondacar to othercar. This is not an error. On the contrary, any superclass reference variable may reference an object of any of its subclasses. What is really interesting is when we print othercar the second time, the toString() method of Honda is the method that is invoked!

This requires a bit of explaining. First let us look at the output of the CarExerciser.java program shown here:

Acura 4 door with hatchback.
Honda Fit SPORT InLine4Cylinder 4 door with hatchback.
Acura 4 door with hatchback.
Honda Fit SPORT InLine4Cylinder 4 door with hatchback.

The Java runtime environment knows that othercar is pointing to an object of Honda and not to an object of Car. As a result we call the toString() method of the referred object, not the method of the reference variable class type. This particular phenomenon is called late binding (also dynamic binding, runtime binding and virtual method invocation).

Essentially, the method to call is determined at runtime, not compile time. Every subclass is allowed to have its own toString() method, and the meaning of the call to toString() is based on the object referenced and not the reference variable. The ability for us to assign multiple meanings to a method is called polymorphism. Polymorphism is implemented by using late binding.

The reference variable othercar can point to any object of class type Car or the class Honda. Because othercar can have many forms, we say that othercar is a polymorphic reference variable.

Look closely at the conditional tests that use another reserved word called instanceof. This reserved word is an operator, and we can use this operator at runtime to determine what a reference variable refers to. This way, we can be confident that actions are on the appropriate object.

If polymorphism is something you wish to stop, you can prohibit the overriding of a method by declaring the method of the superclass as final. You can also stop the ability to create a subclass of a given class by declaring the class as final. There are some exceptions to late binding as well; Java does not use late binding on static, final, or private methods.


The Object Class

Recall that every class has a default toString() method even if you do not provide one. Also recall that the default value of a class printed with the toString() method provided is the class name and a hash value (not an address).

So where did this default toString() method come from? It comes from the standard class Object. This class is defined in the package java.lang, which is the package that every Java program imports even when you have no import statements. In addition, when you define a class that has no extends keyword, your class automatically becomes a subclass of the class Object. One way or another, your class will be a subclass of the ultimate superclass, Object. Table 1 shows some members of the class Object.

Useful members of the class Object
public Object()
Object constructor.
protected Object clone()
Returns a object that is a copy of this object.
public boolean equals(Object obj)
Returns true if obj and this object refer to the same memory space.
protected void finalize()
This method is called when an object goes out of scope.
public int hashCode()
Returns the integer representation of the object. Especially useful for hash tables supported by java.util.Hashtable.
public String toString()
Returns a string representation of the object.

Table 1: Members of the Object class.

Since every class is a subclass of Object, we already know that all of the methods of Object are available to our classes when we define them. We now have a greater understanding of object hierarchies and can better understand some of the class lineages we see in the Java documentation.


Abstract Methods and Classes

A method that has only a heading and no body is known as an abstract method. The heading ends in semicolon and includes the reserved word abstract. An abstract class is also declared with the reserved word abstract. Below are some rules about abstract classes:

  • An abstract class may contain abstract methods.
  • An abstract class may contain nonabstract methods, constructors, finalizers and instance variables.
  • Any class containing abstract methods must be declared abstract.
  • Abstract class objects cannot be instantiated, although you can have an abstract class type reference variable.
  • You may instantiate an object of a subclass of an abstract class, provided the subclass defines all of the abstract methods of the superclass.

The primary purpose of abstract classes is to force the definition of subclasses as direct descendants of the superclass and to force these subclasses to support the abstract methods the superclass declares.

Why would you want to do this? Consider the Car class from earlier. Car could stand on its own as a class. Honda was another class that was derived from Car, but there is nothing tightly coupling the two as a cohesive unit other than ancestry.

What if we devised a more purposeful Car class that allowed for subclasses to be defined but forced them all to have some things in common. This is where an abstract class can come in handy. The negative side of declaring new subclasses of Car is that there is nothing to stop each developer of a subclass from naming its accessor and mutator methods differently.

Imposing the use of abstract methods makes all subclasses consistent. Consider the following:


Interfaces

Recall that the ActionListener (Swing) and the EventHandler (JavaFX) class is a special form of class called an interface. Interfaces are completely abstract classes. We will discover later that there are other interesting interfaces defined within Java. Reember that Java only supports single inheritance. Therefore we can only create classes that are derivatives of one superclass.

This is precisely the reason we have interfaces. Java will allow us to define a class using more than one interface. Our GUI programs implemented an event handling mechanism using what is known as inner classes, anonymous classes, and even Lamba expressions.

Interfaces are attached to your program by using the implements reserved word. See Example 5b of Chapter 7a and Example 10a of Chapter 7b for examples of main classes implement the interface instead of using inner or anonymous classes.

Essentially an interface is a class that declares only abstract methods. They may also contain named constants.

Remember that any class which implements an interface must provide definitions for all of the methods of the interface, or you will not be able to instantiate an object of that class.

Another useful aspect of interfaces is the ability to declare a reference variable of an interface type and then reference any object type that implements that interface. Consider the code in Example 4.

Lists.java
import java.util.List;
import java.util.Vector;
import java.util.ArrayList;
import java.util.Arrays;

public class Lists {
    public static void main(String[] args) {
        List<String> l;

        l = new Vector(Arrays.asList("red", "bed", "led"));
        System.out.println(l);

        l = new ArrayList(Arrays.asList("abc", "def", "ghi"));
        System.out.println(l);
    }
}

Example 4: Using an interface to reference different objects.

The reason the reference variable l can point to so many different object types is because all of those classes implement List<E>. The benefit of this is we can declare reference variables and method parameters of type List<E> and not worry about which type is assigned because we will able to perform List specific actions.


Inheritance Versus Composition

We are trying to tell the difference between an is-a relationship and a has-a relationship. With inheritance, we are saying that the subclass is-a superclass, or using our examples from earlier, Honda is-a Car since it is a derivative class.

What comes about later is we define classes that have other classes as members within the class. For example, in Car, there is a String instance variable called make. So we can say that Car has-a String.

Just as every person has a name. The simplest way to represent this is a class called Person with an instance variable called name. For example:

public class Person {
    String name;
    // rest of class definition
}

This way, we show composition or the relationship between the two classes. String is a class defined inside of Person.


Exercises

  1. (Beginner) Write a Java program to extend a base class Shape with method getArea().
    public class Shape {
        double getArea() {
            return 0; // Default
        }
    }
    

    Create three subclasses: Circle, Rectangle, and Triangle. Override the getArea() method in each subclass to calculate and return the shape’s area.

    • For Circle, also add getCircumference().
    • For Rectangle, also add getPerimeter().

    Feel free to enhance as you see fit.

  2. (Intermediate) Create an Animal class. Define only a name. Create Livestock and Pet classes as subclasses of Animal. Define characteristics for Livestock such as purpose (which could be an enum) and value. Define characteristics for Pet such as personality and size (more enums?). Now begin to determine how Dog, Cat, Chicken, Cow, etc. might look. Consider other characteristics like purpose (meat, protection, egg production, companionship, etc.)
  3. (Advanced) Create BankAccount with acctNumber and balance. It has methods deposit(), withdraw(), and getBalance(). Create SavingsAccount (extending BankAccount) with interestRate and method calculateInterest(). Create CheckingAccount (extending BankAccount) with overdraftLimit and override withdraw() to accommodate the overdraft.

    Create a file of transactions to apply to the accounts you’ve designed. Include account creation, withdrawal, deposit, and check for errors.

Post navigation

❮ Previous Post: Chapter 10 – Vector, ArrayList and Enumerations
Next Post: Chapter 12 – Exceptions ❯

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Copyright © 2018 – 2025 Programming by Design.