Inheritance

Reviewed & published by Brayan K

Imagine you have Dog, Cat, and Bird classes. Each needs a name, age, eat(), and sleep(). Without inheritance, you'd copy-paste the same code three times. Inheritance lets you define shared behavior once in a parent class (Animal), and every child class automatically gets it. Write once, reuse everywhere.

Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy: Family Tree

Think of a family tree. A child inherits traits (eye color, height) from their parents, but can also develop unique traits of their own. Similarly, a child class inherits fields and methods from a parent class, but can add new ones or override inherited ones with different behavior.

1️⃣ The Problem Inheritance Solves

Without inheritance, you'd duplicate code across similar classes:

// ❌ WITHOUT inheritance — massive code duplication!
class Dog {
    String name;      // duplicated
    int age;          // duplicated
    void eat() { }    // duplicated
    void sleep() { }  // duplicated
    void bark() { }   // unique to Dog
}

class Cat {
    String name;      // duplicated!
    int age;          // duplicated!
    void eat() { }    // duplicated!
    void sleep() { }  // duplicated!
    void purr() { }   // unique to Cat
}

// ✅ WITH inheritance — shared code written ONCE
class Animal {
    String name;
    int age;
    void eat() { System.out.println(name + " eats"); }
    void sleep() { System.out.println(name + " sleeps"); }
}

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

class Cat extends Animal {
    void purr() { System.out.println(name + " purrs..."); }
}

🔑 The extends keyword: class Dog extends Animal means "Dog is a type of Animal and inherits all its fields and methods." Dog automatically gets name, age, eat(), and sleep() without writing them again.

2️⃣ The super Keyword — Talking to the Parent

super refers to the parent class. You use it in two main places:

class Animal {
    String name;
    int age;

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

    void speak() {
        System.out.println(name + " makes a sound");
    }
}

class Dog extends Animal {
    String breed;

    // 1. super() in constructor — MUST be first line!
    Dog(String name, int age, String breed) {
        super(name, age);    // Calls Animal's constructor
        this.breed = breed;  // Then set Dog-specific field
    }

    // 2. super.method() — call parent's version
    @Override
    void speak() {
        super.speak();  // "Rex makes a sound"
        System.out.println(name + " barks! 🐕");
    }
}

⚠️ Critical rule: If the parent class has no default (no-argument) constructor, you must call super(...) as the very first statement in the child's constructor. The compiler will refuse to compile otherwise.

3️⃣ Method Overriding — Customizing Inherited Behavior

A child class can override a parent's method to provide its own implementation. The method signature (name, parameters, return type) must match exactly. Always use the @Override annotation for safety.

class Animal {
    void speak() {
        System.out.println("Some generic animal sound");
    }
}

class Dog extends Animal {
    @Override  // Tells compiler: "I'm intentionally replacing parent's version"
    void speak() {
        System.out.println("Woof! 🐕");
    }
}

class Cat extends Animal {
    @Override
    void speak() {
        System.out.println("Meow! 🐱");
    }
}

// Each type speaks differently:
new Animal().speak();  // "Some generic animal sound"
new Dog().speak();     // "Woof! 🐕"
new Cat().speak();     // "Meow! 🐱"

💡 Why @Override? Without it, a typo like speek() would create a new method instead of overriding speak() — a silent, hard-to-find bug. @Override tells the compiler to verify the parent actually has this method.

// Parent class — shared fields and behavior live here once.
class Animal {
    String name;
    int age;

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

    String eat()   { return name + " is eating"; }
    String sleep() { return name + " is sleeping"; }
    String speak() { return name + " makes a sound"; }
    String info()  { return name + " (age " + age + ")"; }
}

// Child classes inherit everything from Animal and add/override behavior.
class Dog extends Animal {
    String breed;
    Dog(String name, int age, String breed) {
        super(name, age);          // call the parent constructor first
        this.breed = breed;
    }
    @Override String speak() { return name + " barks!"; }   // override
    String fetch() { return name + " fetches the ball!"; }  // new method
}

class Cat extends Animal {
    Cat(String name, int age) { super(name, age); }
    @Override String speak() { return name + " meows!"; }
    String purr() { return name + " purrs..."; }
}

class Bird extends Animal {
    Bird(String name, int age) { super(name, age); }
    @Override String speak() { return name + " chirps!"; }
    String fly() { return name + " is flying!"; }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("Rex", 5, "Golden Retriever");
        Cat cat = new Cat("Whiskers", 3);
        Bird bird = new Bird("Tweety", 1);

        System.out.println("1. INDIVIDUAL OBJECTS:");
        System.out.println("   " + dog.info() + " - " + dog.breed);
        System.out.println("   " + dog.speak());
        System.out.println("   " + dog.eat());    // inherited from Animal
        System.out.println("   " + dog.fetch());  // Dog-only method
        System.out.println("   " + cat.speak());
        System.out.println("   " + cat.purr());    // Cat-only method
        System.out.println("   " + bird.speak());
        System.out.println("   " + bird.fly());     // Bird-only method

        System.out.println("2. INHERITED METHODS (eat):");
        Animal[] animals = { dog, cat, bird };
        for (Animal a : animals) System.out.println("   " + a.eat());
    }
}

4️⃣ Polymorphism — "Many Forms"

Polymorphism is the most powerful concept in OOP. It means a parent type variable can hold any child type object, and method calls automatically use the child's version. Think of it like a universal TV remote — the "power" button does something different on every TV brand.

// The parent type can hold ANY child object
Animal myPet = new Dog("Rex", 5, "Lab");
myPet.speak();  // "Woof! 🐕" — calls Dog's version!
myPet.eat();    // "Rex is eating" — inherited from Animal

// Process different animals uniformly
Animal[] zoo = {
    new Dog("Rex", 5, "Lab"),
    new Cat("Whiskers", 3),
    new Bird("Tweety", 1)
};

for (Animal a : zoo) {
    a.speak();  // Each animal speaks differently!
}
// → "Rex barks!" / "Whiskers meows!" / "Tweety chirps!"

🔑 Why this is powerful: You can write methods that accept Animal and they automatically work with Dog, Cat, Bird, and any future animal class you create. You don't need to change the method — it just works. This is the foundation of extensible software.

5️⃣ instanceof — Type Checking at Runtime

Sometimes you have a parent type variable but need to know the actual type at runtime. Use instanceof:

Animal pet = new Dog("Rex", 5, "Lab");

pet instanceof Dog     // true  — it IS a Dog
pet instanceof Animal  // true  — it's also an Animal
pet instanceof Cat     // false — it's NOT a Cat

// Useful for type-specific actions:
if (pet instanceof Dog) {
    Dog d = (Dog) pet;  // "Casting" — now we can call Dog methods
    d.fetch();           // This method only exists on Dog
}

// Java 16+ pattern matching (cleaner):
if (pet instanceof Dog d) {
    d.fetch();  // No separate cast needed!
}
abstract class Shape {
    abstract double area();
    String describe() {
        return getClass().getSimpleName()
            + " -> area = " + String.format("%.2f", area());
    }
}

class Circle extends Shape {
    double radius;
    Circle(double radius) { this.radius = radius; }
    @Override double area() { return Math.PI * radius * radius; }
}

class Rectangle extends Shape {
    double width, height;
    Rectangle(double width, double height) { this.width = width; this.height = height; }
    @Override double area() { return width * height; }
}

class Triangle extends Shape {
    double base, height;
    Triangle(double base, double height) { this.base = base; this.height = height; }
    @Override double area() { return 0.5 * base * height; }
}

public class Main {
    public static void main(String[] args) {
        // A Shape[] holds ANY subclass — that's polymorphism.
        Shape[] shapes = {
            new Circle(5),
            new Rectangle(10, 4),
            new Triangle(8, 6),
            new Circle(3)
        };

        System.out.println("1. POLYMORPHISM (same area() method):");
        Shape largest = shapes[0];
        double total = 0;
        for (Shape s : shapes) {
            System.out.println("   " + s.describe());
            total += s.area();
            if (s.area() > largest.area()) largest = s;
        }

        System.out.println("2. LARGEST: " + largest.describe());
        System.out.println("3. TOTAL AREA: " + String.format("%.2f", total));

        System.out.println("4. TYPE CHECKING (instanceof):");
        for (Shape s : shapes) {
            if (s instanceof Circle c) {   // Java 16+ pattern matching
                System.out.println("   Circle with radius " + c.radius);
            }
        }
    }
}

6️⃣ Inheritance vs Composition

This is one of the most important design decisions you'll make. The test is simple:

Inheritance ("is-a") ✅

Dog is an Animal. Circle is a Shape.

class Dog extends Animal { }
class Circle extends Shape { }

Composition ("has-a") ✅

Car has an Engine. House has a Kitchen.

class Car {
    private Engine engine; // field
}

❌ Bad inheritance: Stack extends ArrayList — a Stack is NOT a type of ArrayList. It just happens to use a list internally. Use composition instead.

💡 Pro rule: "Favor composition over inheritance." Deep inheritance hierarchies (5+ levels) become rigid and hard to change. Use 1-2 levels of inheritance + interfaces for flexibility.

7️⃣ Access Modifiers in Inheritance

Not all parent members are accessible to child classes. Here's what each access modifier allows:

ModifierSame ClassSubclassOther Classes
public✅✅✅
protected✅✅❌
(default)✅Same pkgSame pkg
private✅❌❌

Use protected for fields/methods that subclasses need but outsiders shouldn't access. It's the sweet spot for inheritance.

class Employee {
    String name;
    int baseSalary;   // monthly
    Employee(String name, int baseSalary) { this.name = name; this.baseSalary = baseSalary; }

    int getAnnualSalary() { return baseSalary * 12; }
    String getRole() { return "Employee"; }

    @Override public String toString() {
        return String.format("%-10s %-12s $%d", name, getRole(), getAnnualSalary());
    }
}

class Manager extends Employee {
    int bonus;
    Manager(String name, int salary, int bonus) { super(name, salary); this.bonus = bonus; }
    @Override int getAnnualSalary() { return super.getAnnualSalary() + bonus; }
    @Override String getRole() { return "Manager"; }
}

class Developer extends Employee {
    String language;
    Developer(String name, int salary, String language) { super(name, salary); this.language = language; }
    @Override String getRole() { return "Developer"; }
    String code() { return name + " is coding in " + language; }
}

class Intern extends Employee {
    Intern(String name, int salary) { super(name, salary); }
    @Override int getAnnualSalary() { return super.getAnnualSalary() / 2; }  // part-time
    @Override String getRole() { return "Intern"; }
}

public class Main {
    public static void main(String[] args) {
        Employee[] team = {
            new Manager("Alice", 8000, 24000),
            new Developer("Bob", 7500, "Java"),
            new Developer("Charlie", 7200, "Python"),
            new Intern("Eve", 3000)
        };

        System.out.println("1. TEAM ROSTER:");
        int totalPayroll = 0;
        Employee highest = team[0];
        for (Employee e : team) {
            System.out.println("   " + e);
            totalPayroll += e.getAnnualSalary();
            if (e.getAnnualSalary() > highest.getAnnualSalary()) highest = e;
        }

        System.out.println("2. TOTAL PAYROLL: $" + totalPayroll);
        System.out.println("3. HIGHEST EARNER: " + highest.name
            + " ($" + highest.getAnnualSalary() + ")");

        System.out.println("4. DEVELOPERS:");
        for (Employee e : team) {
            if (e instanceof Developer d) System.out.println("   " + d.code());
        }
    }
}
public class Main {
    static class Vehicle {
        // 1) Visible to subclasses, but not to the rest of the world.
        ___ String plate;                       // 👉 replace ___ with protected

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

        double dailyRate() { return 25.0; }

        String describe() {
            return getClass().getSimpleName() + " " + plate + " at " + dailyRate() + "/day";
        }
    }

    // 2) A Van IS a Vehicle, and inherits everything above.
    static class Van ___ Vehicle {              // 👉 replace ___ with extends

        // 3) A subclass must hand the parent what its constructor needs.
        Van(String plate) { ___(plate); }       // 👉 replace ___ with super

        // 4) This annotation makes the compiler check you really are replacing
        //    a method — a typo in the name becomes an error instead of a bug.
        ___
        double dailyRate() {                    // 👉 replace ___ with @Override
            // 5) Reuse the parent's answer instead of repeating the number.
            return ___.dailyRate() * 2;         // 👉 replace ___ with super
        }
    }

    public static void main(String[] args) {
        // One array, two different classes, and each picks its own dailyRate.
        Vehicle[] fleet = { new Vehicle("AB12 CDE"), new Van("XY99 ZZZ") };
        for (Vehicle v : fleet) {
            System.out.println(v.describe());
        }
        System.out.println("A Van is a Vehicle? " + (fleet[1] instanceof Vehicle));
    }
}

Common Mistakes

Pro Tips

💡 Always use @Override: It's free compile-time error checking. There's no reason not to use it.

💡 Keep hierarchies shallow: 1-2 levels of inheritance is ideal. Beyond 3, consider interfaces + composition.

💡 Program to interfaces, not implementations: Use Animal pet = new Dog() instead of Dog pet = new Dog() when possible — this makes your code more flexible.

💡 Use protected wisely: It's better than public for fields that subclasses need, but consider if a getter method would be even better.

💡 The Liskov Substitution Principle: A child class should work anywhere the parent class is expected. If a method expects Animal, passing a Dog should work correctly.

📋 Quick Reference

KeywordSyntaxPurpose
extendsclass Dog extends AnimalInherit from parent
super()super(name, age)Call parent constructor
super.method()super.speak()Call parent's method version
@Override@Override void speak()Override with safety check
instanceofpet instanceof DogCheck object's actual type
protectedprotected String name;Accessible by subclasses

🎉 Lesson Complete!

You now understand inheritance, method overriding, polymorphism, the super keyword, and when to choose inheritance vs composition! These concepts are the backbone of extensible, maintainable Java applications.

Practice quiz

Which keyword makes a class inherit from a parent class?

  • implements
  • inherits
  • extends
  • super

Answer: extends. 'class Dog extends Animal' makes Dog inherit Animal's fields and methods.

Inside a child constructor, where must super(...) appear if the parent has no default constructor?

  • As the very first statement
  • Anywhere in the body
  • As the last statement
  • It is optional

Answer: As the very first statement. super(...) must be the first statement, or the code will not compile.

What is the main purpose of the @Override annotation?

  • It makes a method run faster
  • It hides the parent method
  • It creates a new method
  • It tells the compiler to verify the parent actually has this method

Answer: It tells the compiler to verify the parent actually has this method. @Override catches typos like speek() at compile time by checking the parent has the method.

Given Animal a = new Dog(); and Dog overrides speak(), what does a.speak() call?

  • Animal's version
  • Dog's version
  • A compile error
  • Neither — it returns null

Answer: Dog's version. Polymorphism: the actual object's (Dog's) overriding method runs.

What does 'pet instanceof Dog' evaluate to when pet refers to a Dog object?

  • true
  • false
  • null
  • A compile error

Answer: true. instanceof checks the object's runtime type; a Dog is a Dog, so it is true.

Which relationship is the correct test for using inheritance?

  • 'has-a'
  • 'uses-a'
  • 'is-a'
  • 'needs-a'

Answer: 'is-a'. Inheritance models 'is-a' (Dog is an Animal); composition models 'has-a'.

Which access modifier lets subclasses access a member but blocks unrelated outside classes?

  • private
  • protected
  • public
  • (default) package-private

Answer: protected. protected is accessible in the same class and subclasses, but not arbitrary other classes.

What does super.speak() do inside an overriding speak() method?

  • Calls the child's speak() again
  • Throws an error
  • Skips the method
  • Calls the parent class's version of speak()

Answer: Calls the parent class's version of speak(). super.method() invokes the parent class's implementation.

A private field in a parent class is accessible from a subclass.

  • True
  • False
  • Only if marked @Override
  • Only via instanceof

Answer: False. The table shows private members are NOT accessible from subclasses.

Why is favoring composition over deep inheritance recommended?

  • Inheritance is always slower
  • Composition removes the need for classes
  • Deep hierarchies (5+ levels) become rigid and hard to change
  • It is required by the compiler

Answer: Deep hierarchies (5+ levels) become rigid and hard to change. Deep inheritance hierarchies become rigid; the lesson advises 1-2 levels plus interfaces.

Continue this course