Module 6 · Inheritance, Polymorphism & Design ⏱ 19 min

Inheritance & Interfaces

By the end of this lesson you will be able to:
  • Use extends to build a subclass on top of an existing class, and super to call the parent
  • Define an interface and implement it in a class
  • Explain polymorphism — why one variable type can hold many object kinds

You rarely model a problem with one flat pile of classes. Some classes are special cases of others (a Student is a kind of Person), and some share a promise about what they can do without sharing any code (anything that can speak). Java gives you inheritance for the first and interfaces for the second.

Both ideas appear constantly in real libraries. ArrayList extends AbstractList; Runnable and Comparable are interfaces you implement every day. Understanding when to use which — and how Java decides which method actually runs — is essential for reading any non-trivial codebase. Getting this wrong leads to brittle hierarchies that break when you change one class, or duplicated code because you failed to extract a common contract.

Interfaces — a promise of behaviour

An interface lists methods a class promises to provide, but contains no code for them. A class says implements Speaker and then writes those methods. The payoff: any code that only needs 'something that can speak' can hold a Speaker and not care what exact kind of object it is.

This is how Java achieves flexibility without fragility. If you write a method that accepts Speaker, you can later invent a Robot that also implements Speaker, pass it in, and your existing method works unchanged. The interface name itself becomes a type: you can declare a variable as Speaker, store a Person in it, and later store a Student in the same variable. The compiler only cares that the object satisfies the contract.

flowchart TD
  SP["Speaker (interface)"] --> P["Person implements Speaker"]
  P -->|extends| S["Student extends Person"]
  style SP fill:#c2410c,color:#fff
  style P fill:#c2410c,color:#fff
An interface declares behaviour; a class implements it. A subclass extends its parent and can reuse the parent's work via super.
Main.java — an interface, a class that implements it, a subclass that extends it, and polymorphism in action. Real, compiling Java.
public class Main {
    interface Speaker {
        String sayHello();
    }

    static class Person implements Speaker {
        private String name;

        Person(String name) {
            this.name = name;
        }

        @Override
        public String sayHello() {
            return "Hi, I'm " + name;
        }
    }

    static class Student extends Person {
        private String course;

        Student(String name, String course) {
            super(name);
            this.course = course;
        }

        @Override
        public String sayHello() {
            return super.sayHello() + " studying " + course;
        }
    }

    public static void main(String[] args) {
        Speaker a = new Person("Lia");
        Speaker b = new Student("Owen", "Physics");
        System.out.println(a.sayHello());
        System.out.println(b.sayHello());
    }
}

Inheritance and super

extends makes one class build on another: a Student automatically gets every field and method a Person has, and can add more. The subclass's constructor uses super(...) to hand arguments up to the parent's constructor. If you omit it, Java inserts a call to super() with no arguments — which fails if the parent has no such constructor.

When a subclass provides its own version of a method the parent already has, that's overriding — and the @Override annotation tells the compiler 'I mean to replace a parent method; please check that I really am'. Overriding is not the same as overloading: overloading creates a new method with different parameters; overriding replaces the parent's method with the same signature. The @Override annotation catches the mistake of accidentally overloading when you meant to override.

flowchart TD
  P["Person(name)"] -->|"extends"| S["Student(name, course)"]
  S -->|"super(name)"| P
  style P fill:#c2410c,color:#fff
  style S fill:#c2410c,color:#fff
A subclass constructor must pass the parent's required data up via super(...).
Main.java — inheritance with fields, super, and overriding. Real, compiling Java.
public class Main {
    static class Vehicle {
        private String brand;

        Vehicle(String brand) {
            this.brand = brand;
        }

        String describe() {
            return "A vehicle by " + brand;
        }
    }

    static class Car extends Vehicle {
        private int doors;

        Car(String brand, int doors) {
            super(brand);
            this.doors = doors;
        }

        @Override
        String describe() {
            return super.describe() + " with " + doors + " doors";
        }
    }

    public static void main(String[] args) {
        Car c = new Car("Toyota", 4);
        System.out.println(c.describe());
    }
}

Polymorphism — many shapes, one interface

Both a and b in our earlier example are declared as Speaker — yet b is actually a Student, so its overridden sayHello runs, not Person's. Java picks the method based on the object's real type at runtime, not the variable's declared type. One interface, many shapes — that's polymorphism.

This is powerful because it lets you write general code. A method that takes Speaker does not need a separate branch for Person, Student, Robot, or anything else you invent later. You add new classes; the existing method handles them automatically. That extensibility is why object-oriented design dominates large systems. The cost is indirection: to know which method runs, you must trace the object's actual class. In a small program this is obvious; in a large one, the new call may be twenty files away from the invocation.

flowchart LR
  V["Speaker s = new Student(...)"] --> D{"Object's real type?"}
  D -->|Person| P["Person.sayHello()"]
  D -->|Student| S["Student.sayHello()"]
  style D fill:#c2410c,color:#fff
Dynamic dispatch: Java looks at the object's real type to decide which method to run.
Main.java — polymorphism with an array of interface types. Real, compiling Java.
public class Main {
    interface Drawable {
        String draw();
    }

    static class Circle implements Drawable {
        @Override
        public String draw() {
            return "Drawing a circle";
        }
    }

    static class Square implements Drawable {
        @Override
        public String draw() {
            return "Drawing a square";
        }
    }

    public static void main(String[] args) {
        Drawable[] shapes = {new Circle(), new Square()};
        for (Drawable d : shapes) {
            System.out.println(d.draw());
        }
    }
}

Interface versus abstract class

An interface is a pure contract: no fields, no method bodies (before Java 8). An abstract class is a partial implementation: it can have fields, concrete methods, and abstract ones. Use an interface when unrelated classes need to share a capability. Use an abstract class when several classes are genuinely related and share common code.

Java allows a class to implement many interfaces but extend only one class — so interfaces give you more freedom. If you find yourself forcing an is-a relationship just to share code, consider whether an interface with a helper class would be cleaner.

Exercise

What does this program print? Read it carefully and type the exact output (two lines).

public class Main {
    interface Speaker {
        String sayHello();
    }

    static class Person implements Speaker {
        private String name;

        Person(String name) {
            this.name = name;
        }

        @Override
        public String sayHello() {
            return "Hi, I'm " + name;
        }
    }

    static class Student extends Person {
        private String course;

        Student(String name, String course) {
            super(name);
            this.course = course;
        }

        @Override
        public String sayHello() {
            return super.sayHello() + " studying " + course;
        }
    }

    public static void main(String[] args) {
        Speaker a = new Person("Lia");
        Speaker b = new Student("Owen", "Physics");
        System.out.println(a.sayHello());
        System.out.println(b.sayHello());
    }
}
Exercise

What does the @Override annotation do?

Exercise

What does this program print? Read it carefully and type the exact output (one line).

public class Main {
    static class Vehicle {
        private String brand;

        Vehicle(String brand) {
            this.brand = brand;
        }

        String describe() {
            return "A vehicle by " + brand;
        }
    }

    static class Car extends Vehicle {
        private int doors;

        Car(String brand, int doors) {
            super(brand);
            this.doors = doors;
        }

        @Override
        String describe() {
            return super.describe() + " with " + doors + " doors";
        }
    }

    public static void main(String[] args) {
        Car c = new Car("Toyota", 4);
        System.out.println(c.describe());
    }
}
Exercise

What does this program print? Read it carefully and type the exact output (two lines).

public class Main {
    interface Drawable {
        String draw();
    }

    static class Circle implements Drawable {
        @Override
        public String draw() {
            return "Drawing a circle";
        }
    }

    static class Square implements Drawable {
        @Override
        public String draw() {
            return "Drawing a square";
        }
    }

    public static void main(String[] args) {
        Drawable[] shapes = {new Circle(), new Square()};
        for (Drawable d : shapes) {
            System.out.println(d.draw());
        }
    }
}
Exercise

A class Student extends Person and does NOT override sayHello(). Person implements Speaker and its sayHello() returns "Hi, I'm " + name. What does new Student("Owen").sayHello() return?

The root of every class: Object

If you do not write extends, Java silently adds extends Object. This means every class inherits methods like toString(), equals(), and hashCode(). Overriding toString() is one of the most common tasks in Java because it controls how your object looks when printed. The default implementation prints the class name and a memory address, which is rarely useful.

Understanding that every class ultimately extends Object explains why you can store any object in a variable of type Object. It is the universal superclass, and the foundation on which collections, generics, and reflection are built.

Recap

  • An interface declares behaviour; a class implements it to promise that behaviour.
  • Inheritance (extends) reuses a parent's code; overriding replaces a parent's method.
  • super(...) calls the parent's constructor; it must be the first statement if you write it.
  • Polymorphism means Java calls the object's real method at runtime, not the variable's declared type.
  • Prefer interfaces for shared capabilities across unrelated classes; prefer abstract classes for related classes with shared code.

Next you will explore Java's collections — lists, maps, and sets — which use exactly these polymorphic interfaces to give you flexible, reusable data structures.

Checkpoint quiz

What is the difference between extends and implements?

A variable is declared as Speaker but holds a Student object. Which sayHello runs?

Go deeper — technical resources