Inheritance & Polymorphism

Java · Lesson 17 of 32 7 min read Updated

Many things in the real world are special versions of other things. A manager is an employee with extra responsibilities. A car, a truck and an auto-rickshaw are all vehicles. Inheritance lets one class build on another: the new class gets all the fields and methods of the existing one and adds or changes what it needs.

Polymorphism (Greek for "many forms") is what makes inheritance powerful: code written for the general type, such as Vehicle, automatically works with every special type, and each object behaves in its own way. In this lesson you will use both, and learn how to cast safely with instanceof patterns.

extends and super

A subclass (child class) declares its parent with extends. The parent is called the superclass. The subclass inherits the parent's accessible fields and methods, and its constructor must first run the parent's constructor with super(...).

Java
public class Staff {
    public static void main(String[] args) {
        Employee e = new Employee("Rohan", 42000);
        Manager m = new Manager("Priya", 85000, 15000);

        System.out.println(e.describe());
        System.out.println(m.describe());
        System.out.println(m.getName() + " leads a team: " + m.hasTeam());
        System.out.println("Priya's monthly pay: Rs " + m.monthlyPay());
    }
}

class Employee {
    protected final String name;
    protected final double salary;

    Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    String getName() { return name; }

    double monthlyPay() {
        return salary;
    }

    String describe() {
        return name + " earns Rs " + monthlyPay();
    }
}

class Manager extends Employee {
    private final double allowance;

    Manager(String name, double salary, double allowance) {
        super(name, salary);            // run Employee's constructor first
        this.allowance = allowance;
    }

    boolean hasTeam() { return true; }

    @Override
    double monthlyPay() {
        return super.monthlyPay() + allowance;
    }

    @Override
    String describe() {
        return "Manager " + super.describe();
    }
}
Output
Rohan earns Rs 42000.0
Manager Priya earns Rs 100000.0
Priya leads a team: true
Priya's monthly pay: Rs 100000.0

Several ideas meet here:

  • Manager did not declare getName(), yet m.getName() works: it was inherited.
  • hasTeam() is new behaviour that only managers have.
  • monthlyPay() is overridden: Manager provides its own version with the same name and parameters. Inside it, super.monthlyPay() calls the parent's version, so the salary logic is not repeated.
  • describe() lives in Employee and calls monthlyPay(). For a manager, Java runs the manager's monthlyPay, which is why Priya's description includes her allowance. This is polymorphism at work.
  • The fields are protected, so subclasses can read them.

If the parent has no no-argument constructor, the child must call super(...) with arguments; otherwise the compiler reports error: constructor Employee in class Employee cannot be applied to given types. Java 25's flexible constructor bodies (see Constructors) also allow checks before super(...), so you can reject bad arguments before the parent constructor runs.

@Override

Always put @Override on a method that is meant to replace a parent method. It asks the compiler to check that a matching method really exists in a superclass. If you mistype the name, say tostring instead of toString, you get error: method does not override or implement a method from a supertype instead of a silent bug.

Polymorphism in action

A variable of a superclass type can hold an object of any subclass. When you call an overridden method through that variable, Java looks at the actual object at run time and calls its version. This is called dynamic dispatch.

Java
public class Fleet {
    public static void main(String[] args) {
        Vehicle[] fleet = {
            new Vehicle("HR12AB1234"),
            new Car("RJ14CD5678", true),
            new Truck("UP32EF9012", 8),
            new AutoRickshaw("KL07GH3456")
        };

        int km = 20;
        for (Vehicle v : fleet) {
            System.out.printf("%-14s %-13s Rs %6.1f%n",
                    v.getClass().getSimpleName(), v.registration, v.fare(km));
        }
    }
}

class Vehicle {
    final String registration;

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

    double fare(int km) {
        return 10.0 * km;
    }
}

class Car extends Vehicle {
    private final boolean airConditioned;

    Car(String registration, boolean airConditioned) {
        super(registration);
        this.airConditioned = airConditioned;
    }

    @Override
    double fare(int km) {
        double base = 14.0 * km;
        return airConditioned ? base * 1.1 : base;
    }
}

class Truck extends Vehicle {
    private final int tonnes;

    Truck(String registration, int tonnes) {
        super(registration);
        this.tonnes = tonnes;
    }

    @Override
    double fare(int km) {
        return 25.0 * km + 100.0 * tonnes;
    }
}

class AutoRickshaw extends Vehicle {
    AutoRickshaw(String registration) {
        super(registration);
    }

    @Override
    double fare(int km) {
        return 30 + 12.0 * Math.max(0, km - 2);
    }
}
Output
Vehicle        HR12AB1234    Rs  200.0
Car            RJ14CD5678    Rs  308.0
Truck          UP32EF9012    Rs 1300.0
AutoRickshaw   KL07GH3456    Rs  246.0

The loop knows only about Vehicle, yet each object calculates its fare its own way. Tomorrow you can add an ElectricBus class without changing the loop at all. This is the real benefit of polymorphism: code that works with the general type stays the same while new special types are added.

Casting and instanceof patterns

Putting a Dog into an Animal variable is an upcast, and it is automatic. Going back down, from Animal to Dog, is a downcast and needs care: the object might not be a dog at all. The instanceof operator tests the type, and since Java 16 it can also declare a variable of the tested type in one step. This is called a type pattern.

Java
public class Casting {
    static void inspect(Animal a) {
        a.speak();
        if (a instanceof Dog d) {          // test and cast in one step
            d.fetch();
        } else if (a instanceof Cat c && c.lives > 5) {
            System.out.println("A cat with " + c.lives + " lives left");
        }
    }

    public static void main(String[] args) {
        Animal pet = new Dog();     // upcast: automatic
        inspect(pet);
        inspect(new Cat());

        Object o = new Cat();
        System.out.println(o instanceof Animal);
        System.out.println(o instanceof Dog);
    }
}

class Animal {
    void speak() { System.out.println("..."); }
}

class Dog extends Animal {
    @Override
    void speak() { System.out.println("Woof"); }
    void fetch() { System.out.println("Fetching the ball"); }
}

class Cat extends Animal {
    int lives = 9;
    @Override
    void speak() { System.out.println("Meow"); }
}
Output
Woof
Fetching the ball
Meow
A cat with 9 lives left
true
false

Through an Animal variable you can only call methods that Animal declares; fetch() needs a Dog. If you force a wrong cast, such as (Dog) new Cat() stored in an Animal, the code compiles but fails at run time with java.lang.ClassCastException: class Cat cannot be cast to class Dog. The pattern form a instanceof Dog d avoids that, because d exists only when the test succeeds.

The Object class, equals and hashCode

Every class that does not extend anything else extends java.lang.Object. That is where toString, equals and hashCode come from. The inherited equals behaves like == (same object), so two coins with the same value and year are "not equal" until you override it. Whenever you override equals, also override hashCode, so that equal objects produce equal hash codes; collections such as HashSet and HashMap depend on this.

Java
import java.util.Objects;

public class Eq {
    public static void main(String[] args) {
        Coin a = new Coin(10, 2019);
        Coin b = new Coin(10, 2019);
        System.out.println(a == b);
        System.out.println(a.equals(b));
        System.out.println(a.hashCode() == b.hashCode());
    }
}

class Coin {
    private final int value;
    private final int year;

    Coin(int value, int year) {
        this.value = value;
        this.year = year;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) return true;
        if (!(other instanceof Coin c)) return false;
        return value == c.value && year == c.year;
    }

    @Override
    public int hashCode() {
        return Objects.hash(value, year);
    }
}
Output
false
true
true

Writing these methods by hand is tedious. For classes that simply hold data, Java's records generate equals, hashCode and toString for you; see Records & Enums.

Rules and limits

  • Java allows single inheritance of classes: a class extends exactly one parent. A class can implement many interfaces, as you will see in Abstract Classes & Interfaces.
  • A final class cannot be extended (String is final: class MyString extends String gives error: cannot inherit from final String). A final method cannot be overridden.
  • Overloading means same name, different parameters, chosen at compile time. Overriding means same name and parameters in a subclass, chosen at run time.
  • An overriding method cannot be less visible than the one it replaces; a public method stays public.

Common mistakes

  • Using inheritance just to reuse code. Inherit only when the child truly is a kind of parent. A Car is a Vehicle, but a Car is not an Engine; give it an Engine field instead (this is called composition).
  • Forgetting @Override. A typo creates a new method instead of overriding.
  • Downcasting without checking. Use instanceof patterns to avoid ClassCastException.
  • Overriding equals but not hashCode. Equal objects will behave strangely in HashSet and HashMap.

Key points

  • class Child extends Parent inherits the parent's fields and methods; Java classes have exactly one parent.
  • A subclass constructor calls super(...) to set up the parent part.
  • Overriding replaces a parent method; mark it with @Override, and use super.method() to reuse the parent's version.
  • Polymorphism: a parent-type variable can hold any subclass object, and the object's own method version runs.
  • x instanceof Type t tests a type and gives a ready-cast variable (Java 16+).
  • Every class extends Object; override equals and hashCode together.

Exercise

Create a class Ticket with a passenger name and a method price() returning 500. Create subclasses SleeperTicket (price 500 + 300) and TatkalTicket (the parent's price plus 30 percent, using super.price()), each with a toString showing the class name, passenger and price. Put one of each in a Ticket[] array with passengers Isha, Farhan and Meera, print them all, and print the total collected.

Show solution
Java
public class Booking {
    public static void main(String[] args) {
        Ticket[] tickets = {
            new Ticket("Isha"),
            new SleeperTicket("Farhan"),
            new TatkalTicket("Meera")
        };
        double total = 0;
        for (Ticket t : tickets) {
            System.out.println(t);
            total += t.price();
        }
        System.out.println("Total collected: Rs " + total);
    }
}

class Ticket {
    protected final String passenger;

    Ticket(String passenger) {
        this.passenger = passenger;
    }

    double price() {
        return 500;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + " for " + passenger + ": Rs " + price();
    }
}

class SleeperTicket extends Ticket {
    SleeperTicket(String passenger) {
        super(passenger);
    }

    @Override
    double price() {
        return 500 + 300;
    }
}

class TatkalTicket extends Ticket {
    TatkalTicket(String passenger) {
        super(passenger);
    }

    @Override
    double price() {
        return super.price() * 1.3;
    }
}
Output
Ticket for Isha: Rs 500.0
SleeperTicket for Farhan: Rs 800.0
TatkalTicket for Meera: Rs 650.0
Total collected: Rs 1950.0

toString is written once in Ticket, but getClass() and price() are resolved on the actual object, so each line shows the right class name and price.

Quick Quiz

1. What does super(name, salary) do in a subclass constructor?

2. Vehicle v = new Truck(...); v.fare(10); Which fare runs if Truck overrides it?

3. What does if (a instanceof Dog d) give you when the test is true?

4. You override equals in a class. What else should you override?

Learn Java with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call