Phase 2 · Object-Oriented JavaModule 9~42 min read

Inheritance & Polymorphism

Model 'is-a' relationships, override behavior, and harness runtime polymorphism and abstract classes.

What you'll learn

Real systems are full of things that are kinds of other things — a savings account is a bank account, a manager is an employee. Inheritance lets classes build on one another, and polymorphism lets you treat many types through one common interface. Together they make OOP powerful.

By the end you'll be able to:

  • Share code between classes with extends
  • Call parent constructors and methods with super
  • Override inherited methods and mark them with @Override
  • Use runtime polymorphism to write flexible code
  • Check and convert types with instanceof and casting
  • Design with abstract classes, and know when to prefer composition

Inheritance

Inheritance lets a subclass (child) reuse the fields and methods of a superclass (parent), then add or change behaviour. You declare it with extends. The subclass automatically gets everything non-private from the parent — no copy-pasting.

An inheritance hierarchy

Animal

name · speak()

Dog · barks()
Cat · meows()
Bird · sings()

Each subclass inherits name & speak(), then specialises.

Model an 'is-a' relationship: a Dog is an Animal.

super & constructor chaining

The super keyword refers to the parent. super(...) calls the parent's constructor (it must be the first line of the child constructor), and super.method() calls the parent's version of a method. When you create a subclass object, constructors run top-down — parent first, then child. This is constructor chaining.

Inheritance.java
class Animal {
    String name;
    Animal(String name) { this.name = name; }
    void speak() { System.out.println(name + " makes a sound"); }
}

class Dog extends Animal {          // Dog inherits from Animal
    Dog(String name) { super(name); }   // call the parent constructor

    @Override
    void speak() { System.out.println(name + " barks"); }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog("Rex");
        d.speak();                  // Dog's version runs
    }
}

Method overriding

A subclass can replace an inherited method by overriding it — same name, same parameters, new body. Always add the @Override annotation: it tells the compiler to verify you really are overriding something, catching typos before they become bugs.

Overriding vs overloading

Overriding replaces a parent method in a subclass (same signature). Overloading (Module 6) is several methods with the same name but different parameters in one class. Different concepts, similar-sounding names.

Runtime polymorphism

Here's the magic. A parent-typed variable can hold any subclass object, and when you call an overridden method, Java runs the actual object's version — decided at runtime, not compile time. This is polymorphism ("many forms"), and it lets you write code against the general type while each object behaves specifically:

Polymorphism.java
Animal[] animals = {
    new Dog("Rex"),
    new Cat("Milo"),
    new Animal("Thing")
};

for (Animal a : animals) {   // all treated as Animal...
    a.speak();               // ...but each runs its OWN speak()
}

Key idea

One loop, one method call — three different behaviours. Add a new Animal subclass later and this loop keeps working unchanged. That flexibility is why polymorphism is so valued.

instanceof & casting

Sometimes you need to know an object's real type or access subclass-specific members. The instanceof operator checks the type, and modern Java can bind a variable in the same step (pattern matching for instanceof), replacing a separate cast:

Casting.java
Animal a = new Dog("Rex");

if (a instanceof Dog d) {    // pattern matching (Java 16+)
    d.speak();               // d is already typed as Dog here
}

System.out.println(a instanceof Cat);   // false

Watch out

Casting to the wrong type ((Cat) someDog) throws a ClassCastException at runtime. Always guard casts with instanceof first — or better, lean on polymorphism so you rarely need to cast at all.

Abstract classes

Sometimes a base class represents a concept too general to instantiate — what is a plain "Shape" with no dimensions? An abstract class can't be instantiated and may declare abstract methods (no body) that every subclass is forced to implement, while still providing shared concrete code:

Abstract.java
abstract class Shape {
    abstract double area();          // no body: subclasses MUST implement

    void describe() {
        System.out.printf("Area = %.2f%n", area());
    }
}

class Circle extends Shape {
    double r;
    Circle(double r) { this.r = r; }

    @Override
    double area() { return Math.PI * r * r; }
}

public class Main {
    public static void main(String[] args) {
        // Shape s = new Shape();   // ERROR: can't instantiate abstract
        Shape s = new Circle(2);
        s.describe();
    }
}

Composition vs inheritance

Inheritance is powerful but easy to overuse. A good check: does the relationship read as "is-a" or "has-a"? A Dog is-a Animal (inheritance fits). A Car has-a Engine — it's built from one, so composition (holding an Engine field) fits better.

is-a vs has-a

Inheritance — "is-a"

Dog is-a Animal

Use when a subtype truly is a kind of the supertype.

Composition — "has-a"

Car has-a Engine

Use when an object is built from other objects. Often the better choice.

Tip

A widely-followed principle: "favour composition over inheritance." Deep inheritance trees become rigid and fragile; composing small, focused objects stays flexible. You'll revisit this in Module 33.

Recap & quick check

Key takeaways

  • extends lets a subclass inherit and specialise a superclass (an 'is-a' relationship).
  • super(...) calls the parent constructor; constructors chain parent-first.
  • Overriding replaces an inherited method (mark it @Override); it's not overloading.
  • Polymorphism: a parent-typed variable runs the actual object's overridden method at runtime.
  • Abstract classes can't be instantiated and force subclasses to implement abstract methods; prefer composition when it's a 'has-a'.

Quick check

1. Which keyword makes one class inherit from another?

2. What does polymorphism let you do?

3. Why add @Override on an overriding method?

4. Can you create an object of an abstract class directly?

5. A Car built from an Engine object is an example of…

Excellent — you can now model rich hierarchies and write flexible, extensible code. Next up: Module 10 — Interfaces, Enums, Records & Nested Types.