Java Inheritance

INTERMEDIATE ~8 min read Tutorial

Inheritance lets one class acquire the fields and methods of another. It models the “is-a” relationship: a Square is a Rectangle, a Car is a Vehicle. Java supports single class inheritance — a class can extend exactly one other class — and unlimited interface implementation.

This tutorial covers the syntax, the super keyword, method overriding, the @Override annotation, constructor chaining, and the modern sealed and permits keywords that let you control who can extend what.

1. The extends Keyword

java
class=class="tok-str">"tok-cmt">// parent (superclass)
public class Animal {
    private String name;

    public Animal(String name) { this.name = name; }
    public String getName()    { return name; }

    public String sound() { return "?"; }
}

class=class="tok-str">"tok-cmt">// child (subclass)
public class Dog extends Animal {
    public Dog(String name) {
        super(name);   class=class="tok-str">"tok-cmt">// call parent constructor
    }

    @Override
    public String sound() {
        return "Woof";
    }
}

class=class="tok-str">"tok-cmt">// usage
Dog rex = new Dog("Rex");
System.out.println(rex.getName());   class=class="tok-str">"tok-cmt">// Rex  - inherited
System.out.println(rex.sound());     class=class="tok-str">"tok-cmt">// Woof - overridden

The subclass automatically gets every non-private field and method from its parent. It can add new ones and override inherited ones. The single-inheritance rule keeps the language sane: there is no dreaded diamond problem from C++.

2. Method Overriding

A subclass can provide its own implementation of a method inherited from a parent:

java
public class Account {
    private double balance;

    public Account(double opening) { this.balance = opening; }
    public double getBalance() { return balance; }

    public void withdraw(double amount) {
        if (amount > balance)
            throw new IllegalArgumentException("insufficient funds");
        balance -= amount;
    }
}

public class SavingsAccount extends Account {
    public SavingsAccount(double opening) { super(opening); }

    @Override
    public void withdraw(double amount) {
        class=class="tok-str">"tok-cmt">// Savings accounts have a $class="tok-num">1 fee per withdrawal
        super.withdraw(amount + class="tok-num">1.0);   class=class="tok-str">"tok-cmt">// call parent's withdraw
    }
}
Always annotate with @Override

If the method does not actually override a parent (typo in name, wrong parameter type), the compiler will tell you. This is one of the cheapest bug-catching tools in the language — use it religiously.

3. The super Keyword

super refers to the parent class. Use it to call the parent's constructor, to call the parent's overridden method, or to access a parent's hidden field:

java
public class Vehicle {
    protected int speed;
    public Vehicle(int speed) { this.speed = speed; }
    public String describe() { return "moving at " + speed + " mph"; }
}

public class Car extends Vehicle {
    private int wheels = class="tok-num">4;

    public Car(int speed, int wheels) {
        super(speed);    class=class="tok-str">"tok-cmt">// must be first statement
        this.wheels = wheels;
    }

    @Override
    public String describe() {
        class=class="tok-str">"tok-cmt">// call the parent's version, then extend
        return super.describe() + " on " + wheels + " wheels";
    }
}

Car c = new Car(class="tok-num">60, class="tok-num">4);
System.out.println(c.describe());   class=class="tok-str">"tok-cmt">// "moving at class="tok-num">60 mph on class="tok-num">4 wheels"

The first statement in a subclass constructor must be either this(...) (chain to another constructor in the same class) or super(...) (chain to a parent constructor). If you write neither, the compiler inserts an implicit super() call to the parent's no-arg constructor — which is an error if the parent has no no-arg constructor.

4. Methods Inherited from Object

Every class extends Object either directly or indirectly. Object defines toString, equals, hashCode, getClass, clone, finalize, and the thread-related wait / notify / notifyAll. Overriding toString, equals, and hashCode is common and important:

java
public class Person {
    private final String name;
    private final int age;

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

    @Override
    public String toString() {
        return "Person[" + name + ", " + age + "]";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person p)) return false;
        return age == p.age && java.util.Objects.equals(name, p.name);
    }

    @Override
    public int hashCode() { return java.util.Objects.hash(name, age); }
}

The contract between equals and hashCode must be honoured: equal objects must have equal hashcodes.

5. final Classes and Methods

Mark a class final to forbid extension; mark a method final to forbid overriding:

java
public final class Money {   class=class="tok-str">"tok-cmt">// cannot be subclassed
    private final long cents;
    public Money(long cents) { this.cents = cents; }
    public Money plus(Money other) { return new Money(this.cents + other.cents); }
}

public class Base {
    public void templateMethod() { class=class="tok-str">"tok-cmt">/* ... */ }
    public final void cannotOverride() { class=class="tok-str">"tok-cmt">/* sensitive code */ }
}

class=class="tok-str">"tok-cmt">// public class Ext extends Money {}   // compile error
class=class="tok-str">"tok-cmt">// public class Sub extends Base { public void cannotOverride() {} } // compile error

Use final on classes that are not designed for inheritance — which is most of them. Effective Java recommends “design for inheritance or prohibit it”.

6. Sealed Classes (Java 17+)

A sealed class explicitly lists which classes may extend it. This gives you a closed inheritance hierarchy — useful for domain modelling and for exhaustive switch expressions:

java
public sealed interface Shape permits Circle, Square, Triangle {}

public record Circle(double radius) implements Shape {
    public double area() { return Math.PI * radius * radius; }
}

public record Square(double side) implements Shape {
    public double area() { return side * side; }
}

public record Triangle(double base, double height) implements Shape {
    public double area() { return base * height / class="tok-num">2; }
}

class=class="tok-str">"tok-cmt">// exhaustive switch - no default needed
public double area(Shape s) {
    return switch (s) {
        case Circle c         -> c.area();
        case Square s2        -> s2.area();
        case Triangle t       -> t.area();
        class=class="tok-str">"tok-cmt">// no default - compiler knows all permitted subclasses
    };
}

Combined with pattern matching for switch, sealed hierarchies let the compiler verify exhaustiveness: if you add a new permitted subclass, every switch over the hierarchy becomes a compile error until you handle the new case.

7. Pitfalls of Inheritance

Inheritance is for “is-a”, not “has-a”

A Stack is not a Vector — it has one. Use composition (a private field of the helper type) instead of inheritance for “has-a” relationships.

Inheritance breaks encapsulation

A subclass depends on the implementation details of its parent. If the parent evolves, the subclass can break. The classic example is HashSet vs a subclass that overrides add to count insertions — the parent's internal addAll calls add, leading to double-counting. Prefer composition plus interfaces unless you control both classes.

Exercises

  1. Create a Vehicle base class with fields speed and brand, plus a move() method. Subclass it as Car and Bicycle, overriding move().
  2. Override toString in a Person class to print “Person[name=X, age=Y]”.
  3. Build a sealed hierarchy Shape permitting Circle, Square, Triangle, and write a switch expression returning the area.
  4. Make a final class Money and explain why you would not want it subclassable.