Lesson 14 — Reusing Classes

Inheritance

Inheritance lets a new class build on an existing one — inheriting its fields and methods, then extending or specialising them. It models real-world is-a relationships and is one of the four pillars of OOP.

The is-a Relationship

Inheritance is appropriate when you can say one type is a specialisation of another. A Learner is a Person. A SavingsAccount is a BankAccount. A Manager is an Employee. When this relationship holds, inheritance lets the subclass reuse everything the superclass defines and add or override what it needs to specialise.

extends Keyword

JAVA — Superclass and subclass
// Superclass (parent) — the general type
public class Person {
    protected String name;  // protected: accessible to subclasses
    protected int    age;

    public Person(String name, int age) {
        this.name = name;
        this.age  = age;
    }

    public void introduce() {
        System.out.println("Hello, I am " + name + ", aged " + age + ".");
    }

    public String getName() { return name; }
    public int    getAge()  { return age; }
}

// Subclass (child) — the specialised type
public class Learner extends Person {

    private String course;
    private double mark;

    // super() calls the parent constructor — must be first statement
    public Learner(String name, int age, String course, double mark) {
        super(name, age);
        this.course = course;
        this.mark   = mark;
    }

    // Add new behaviour specific to Learner
    public void describeStudies() {
        System.out.println(name + " is studying " + course +
            " with a mark of " + mark + "%.");
    }

    public String getCourse() { return course; }
    public double getMark()   { return mark; }
}

Using Inherited Members

JAVA — Using inheritance
Learner l = new Learner("Thandi", 21, "Java", 74.5);

// Inherited from Person:
l.introduce();       // Hello, I am Thandi, aged 21.
System.out.println(l.getName()); // Thandi

// Own methods:
l.describeStudies(); // Thandi is studying Java with a mark of 74.5%.

// A Learner IS-A Person — you can assign it to a Person variable:
Person p = l; // upcasting — legal, always safe
p.introduce(); // works — calls introduce() through Person reference

The super Keyword

super gives you access to the parent class. Use super() to call the parent constructor (must be the first line of the child constructor), and super.methodName() to call a parent method from an overriding child method.

JAVA — super keyword
public class Facilitator extends Person {

    private String specialisation;
    private int    yearsExperience;

    public Facilitator(String name, int age, String spec, int years) {
        super(name, age);  // call Person constructor
        this.specialisation  = spec;
        this.yearsExperience = years;
    }

    // Override introduce() — super.introduce() reuses the parent version
    @Override
    public void introduce() {
        super.introduce(); // "Hello, I am Ms Dube, aged 34."
        System.out.println("I specialise in " + specialisation +
            " with " + yearsExperience + " years of industry experience.");
    }
}

Facilitator f = new Facilitator("Ms Dube", 34, "Software Development", 8);
f.introduce(); // calls overridden version which also calls super

Inheritance Chains and Object

Every class in Java implicitly extends java.lang.Object — the root of the entire class hierarchy. This is why every object has methods like toString(), equals(), and hashCode(). Inheritance chains can be multiple levels deep, but in practice keep them to two or three levels — deep hierarchies become difficult to reason about.

JAVA — Object root and toString
// Visualising the chain:
// Object
//   └─ Person
//        ├─ Learner
//        └─ Facilitator

// toString() comes from Object — override it to get meaningful output
public class Learner extends Person {
    // ... fields, constructor ...

    @Override
    public String toString() {
        return String.format("Learner{name='%s', course='%s', mark=%.1f%%}",
            name, course, mark);
    }
}

Learner l = new Learner("Sipho", 24, "Python", 81.0);
System.out.println(l); // calls toString() automatically

When Not to Use Inheritance

Inheritance is powerful but often misused. Use it only when the is-a relationship is genuine. Do not use it just to share code — composition (giving a class a field of another type) is often cleaner. A Car has-an Engine (composition) — not Car extends Engine.

Practice Task

Your Turn

Build a three-level hierarchy: Person (name, age, introduce()), Learner extends Person (course, mark, describeProgress()), GraduateLearner extends Learner (thesisTitle, supervisor, describeThesis()). Override introduce() at each level to add specialised information using super.introduce(). Create one object of each type, store them all in a Person[] array, and call introduce() on each in a loop.

Common Mistakes

  • Forgetting super() when the parent class has no no-argument constructor — if the parent requires arguments in its constructor, the child must call super(args) as its first line.
  • Making parent fields private instead of protected — private fields are invisible to subclasses.
  • Inheriting when composition is more appropriate — ask yourself honestly: is this really an is-a relationship?
  • Deep inheritance chains — three levels is usually the maximum before the design becomes fragile.
  • Calling super() other than as the first statement — Java requires it first.

Professional Tip

Think of a class hierarchy like a family tree — the parent provides the foundation, the child specialises it. The child inherits everything the parent has but can add and refine. Do not override something just because you can — only override when the child's behaviour genuinely needs to differ.

Mini Quiz

What does the super() call inside a constructor do?