Skip to content
elephantoo

Inheritance

Lesson 14 of 43 16 min read

extends, super, constructor chaining, the Object class, final classes and composition vs inheritance.


Many classes share a lot in common. A SavingsAccount and a CurrentAccount both have a number, an owner and a balance; they differ only in a few rules. Copy-pasting the shared code into both classes means fixing every bug twice. Inheritance lets one class build on another: the new class (the subclass or child) automatically gets the fields and methods of the existing one (the superclass or parent), and then adds or changes what it needs.

extends: an "is-a" relationship#

Zoo.java
public class Zoo {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.name = "Bruno";
        d.eat();      // inherited from Animal
        d.bark();     // defined in Dog

        Cat c = new Cat();
        c.name = "Misty";
        c.eat();
        c.meow();
    }
}

class Animal {
    String name;

    void eat() {
        System.out.println(name + " is eating");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println(name + " says Woof");
    }
}

class Cat extends Animal {
    void meow() {
        System.out.println(name + " says Meow");
    }
}
Output
Bruno is eating
Bruno says Woof
Misty is eating
Misty says Meow

Dog extends Animal reads as "a Dog is an Animal". Every Dog object has a name field and an eat() method without declaring them. This "is-a" test is the most important rule of inheritance: if the sentence sounds wrong ("a Car is an Engine"), inheritance is the wrong tool.

Output
          Animal
       name, eat()
        /       \
     Dog         Cat
   bark()       meow()

Key facts:

  • Java has single inheritance of classes: a class can extend only one parent. (A class can implement many interfaces, covered later.)
  • Inheritance is transitive: if Puppy extends Dog, a Puppy is also an Animal.
  • Constructors are not inherited. Each class declares its own.
  • private members exist inside the child object but the child cannot touch them directly.

Access and inheritance#

Combining inheritance with encapsulation, we keep fields private in the parent and give subclasses methods to work with:

Parent member is...Visible in subclass, same packageVisible in subclass, other package
private❌❌
package-private✅❌
protected✅✅
public✅✅

super(...): constructor chaining#

When you create a Dog, Java first builds the Animal part of the object, then the Dog part. A subclass constructor calls a parent constructor with super(...), which must be the first statement:

Accounts.java
public class Accounts {
    public static void main(String[] args) {
        SavingsAccount s = new SavingsAccount("SB-01", "Asha", 0.04);
        s.deposit(10_000);
        s.addInterest();
        System.out.println(s.getOwner() + " has " + s.getBalance());
    }
}

class Account {
    private final String number;
    private final String owner;
    private double balance;

    Account(String number, String owner) {
        System.out.println("Account constructor");
        this.number = number;
        this.owner = owner;
    }

    void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
        balance += amount;
    }

    double getBalance() { return balance; }
    String getOwner() { return owner; }
    String getNumber() { return number; }
}

class SavingsAccount extends Account {
    private final double rate;

    SavingsAccount(String number, String owner, double rate) {
        super(number, owner);              // build the Account part first
        System.out.println("SavingsAccount constructor");
        this.rate = rate;
    }

    void addInterest() {
        deposit(getBalance() * rate);      // reuse inherited methods; balance itself is private
    }
}
Output
Account constructor
SavingsAccount constructor
Asha has 10400.0

Notice that SavingsAccount never touches balance directly. It goes through deposit() and getBalance(), so the parent's validation still applies.

The implicit super()

If you don't write super(...) (or this(...)), the compiler inserts super();, a call to the parent's no-argument constructor. If the parent does not have one, you get a compile error:

Java
class Person {
    Person(String name) { }      // the only constructor needs a name
}

class Student extends Person {
    Student() { }                // error: constructor Person in class Person cannot be applied to given types
}

Fix it by passing the required arguments: Student() { super("Unknown"); }.

Construction order across several levels

ChainOrder.java
public class ChainOrder {
    public static void main(String[] args) {
        new Puppy();
    }
}

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

class Dog extends Animal {
    Dog() { System.out.println("2. Dog"); }        // implicit super()
}

class Puppy extends Dog {
    Puppy() { System.out.println("3. Puppy"); }    // implicit super()
}
Output
1. Animal
2. Dog
3. Puppy

The chain always runs top-down: the most general part is ready before the specific part is built.

Overriding a method (a first look)#

A subclass can replace an inherited method by declaring one with the same signature. Mark it with @Override so the compiler checks you really are overriding. Use super.method() to call the parent's version:

Shop.java
public class Shop {
    public static void main(String[] args) {
        Product p = new Product("Notebook", 120);
        Product d = new DiscountedProduct("Pen set", 300, 20);
        System.out.println(p.describe());
        System.out.println(d.describe());
    }
}

class Product {
    private final String name;
    private final double price;

    Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    double finalPrice() { return price; }

    String describe() {
        return name + ": Rs " + finalPrice();
    }
}

class DiscountedProduct extends Product {
    private final int percentOff;

    DiscountedProduct(String name, double price, int percentOff) {
        super(name, price);
        this.percentOff = percentOff;
    }

    @Override
    double finalPrice() {
        return super.finalPrice() * (100 - percentOff) / 100;
    }

    @Override
    String describe() {
        return super.describe() + " (" + percentOff + "% off)";
    }
}
Output
Notebook: Rs 120.0
Pen set: Rs 240.0 (20% off)

Look closely at the second line. Product.describe() calls finalPrice(), and because the object is really a DiscountedProduct, the overridden finalPrice() runs. That behaviour is called polymorphism, and it gets the whole next lesson.

The Object class: everyone's parent#

Every class that doesn't say extends implicitly extends java.lang.Object. So every object in Java has these methods (among others):

MethodDefault behaviourUsually overridden?
toString()ClassName@hexHashYes, for readable output
equals(Object o)same as == (same object)Yes, for value classes
hashCode()identity-based numberYes, whenever equals is
getClass()the runtime classNo (it is final)
ObjectDemo.java
public class ObjectDemo {
    public static void main(String[] args) {
        Point p = new Point(3, 4);
        Object o = p;                              // any object fits in an Object variable
        System.out.println(o);                     // calls the overridden toString()
        System.out.println(p.getClass().getSimpleName());
        System.out.println(p instanceof Object);
    }
}

class Point {
    private final int x, y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public String toString() {
        return "Point(" + x + ", " + y + ")";
    }
}
Output
Point(3, 4)
Point
true

toString() is public in Object, so the override must also be public (an override cannot reduce visibility). equals and hashCode have their own lesson later in the course.

final: stopping inheritance#

  • A final class cannot be extended. String, Integer and LocalDate are final, which protects their immutability.
  • A final method cannot be overridden, useful when a subclass must not change a critical rule.
Java
final class Money { /* ... */ }
class FakeMoney extends Money { }            // error: cannot inherit from final Money

class Account {
    final String getNumber() { return "SB-01"; }
}
class SneakyAccount extends Account {
    String getNumber() { return "HACKED"; }   // error: getNumber() in SneakyAccount cannot override getNumber() in Account; overridden method is final
}

Design advice from Effective Java: "Design and document for inheritance or else prohibit it." If you never meant a class to be extended, making it final is a perfectly good choice.

Composition over inheritance#

Inheritance is powerful but creates tight coupling: the child depends on the parent's internals, and a change to the parent can silently break every subclass. Often a better design is composition, where a class holds a reference to another object and delegates work to it.

Garage.java
public class Garage {
    public static void main(String[] args) {
        Car car = new Car("Nexon", new Engine(120));
        car.drive();
    }
}

class Engine {
    private final int horsePower;

    Engine(int horsePower) { this.horsePower = horsePower; }

    void start() {
        System.out.println("Engine (" + horsePower + " hp) started");
    }
}

class Car {
    private final String model;
    private final Engine engine;          // Car HAS-AN Engine

    Car(String model, Engine engine) {
        this.model = model;
        this.engine = engine;
    }

    void drive() {
        engine.start();                   // delegate
        System.out.println(model + " is moving");
    }
}
Output
Engine (120 hp) started
Nexon is moving

With composition you can swap in an ElectricMotor later, test Car with a fake engine, and Car exposes only the methods you choose. A quick guide:

Use inheritance when...Use composition when...
the child truly is a kind of parentthe class has a or uses a helper
subclasses will be used wherever the parent is expectedyou only want to reuse some code
the parent was designed to be extendedthe other class is final or not under your control

A classic warning: class Stack extends ArrayList would expose add(index, x) and remove(index), letting anyone break the stack rules. A Stack that contains a list exposes only push, pop and peek.

Common mistakes#

  • Using inheritance just to reuse code when there is no real "is-a" relationship.
  • Forgetting that the parent needs a no-argument constructor when the child doesn't call super(...).
  • Putting super(...) anywhere but the first line of the constructor.
  • Making parent fields protected so children can poke at them; prefer private fields plus methods.
  • Forgetting @Override and accidentally creating a new method (for example tostring() with a lower-case s).

What's next#

You have seen that the object's real type decides which overridden method runs. Next, polymorphism explains exactly how that works, along with overloading, upcasting, downcasting and instanceof.

Check your understanding

Quick quiz

0/3 answered
  1. 1.A subclass constructor does not call super(...) explicitly. What does Java do?

  2. 2.Which members does a subclass in a different package inherit and access directly?

  3. 3.You want a Car to have an Engine. What relationship fits best?

Finished reading?

Mark this lesson complete to track your progress.