☕ Java

Hierarchical Inheritance

Hierarchical inheritance occurs when multiple classes extend a single superclass — one parent, many children. This is the tree structure that most naturally emerges when modelling real-world taxonomies: Shape has Circle, Rectangle, and Triangle; Animal has Dog, Cat, and Bird; Vehicle has Car, Truck, and Motorcycle. Each subclass independently inherits the common contract of the superclass and specialises it in its own way. This entry covers the structure and benefits of hierarchical inheritance, how polymorphism is maximised through a shared superclass, abstract classes as the ideal superclass for hierarchical designs, how sibling classes remain independent, and design patterns that emerge naturally from this structure.

One Parent, Many Children — The Tree Structure

Hierarchical inheritance forms a tree rooted at the superclass, with each branch representing a subclass. Unlike multilevel inheritance which forms a chain, hierarchical inheritance forms a fan. The superclass sits at the root and defines the common contract: the fields all subclasses share and the methods all subclasses must support (either inherited or overridden). Each subclass independently inherits this foundation and builds on it in its own direction. The defining characteristic of hierarchical inheritance is that sibling classes — subclasses that share the same parent — are completely independent of each other. Dog and Cat both extend Animal, but Dog knows nothing about Cat and Cat knows nothing about Dog. Neither has any access to the other's fields or methods. The only shared ground is the Animal superclass. This independence is what makes hierarchical inheritance the natural model for taxonomies: species share common ancestry but are otherwise independent. The superclass in a hierarchical structure carries a heavy responsibility because it affects every subclass. A field added to Animal adds memory to every Dog, Cat, Bird, and Fish object. A method added to Animal must work correctly for all of them. A change to Animal's constructor forces changes to every subclass constructor. This is why abstract classes — which cannot be instantiated directly and are designed explicitly as inheritance bases — are the ideal superclass in hierarchical designs.
Java
// ── Hierarchical structure ────────────────────────────────────────────
//
//                  Animal  (superclass)
//                /    |        //             Dog    Cat   Bird  Fish   (subclasses — siblings)
//
// Each subclass inherits Animal, but siblings know nothing of each other

public abstract class Animal {

    private final String  name;
    private final int     lifeExpectancyYears;
    protected     boolean awake;

    protected Animal(String name, int lifeExpectancyYears) {
        this.name                = name;
        this.lifeExpectancyYears = lifeExpectancyYears;
        this.awake               = true;
    }

    // ── Abstract — EVERY subclass MUST provide its own implementation ─
    public abstract String makeSound();
    public abstract String move();
    public abstract String diet();

    // ── Concrete — shared behaviour all animals have ──────────────────
    public void sleep() {
        awake = false;
        System.out.println(name + " falls asleep.");
    }

    public void wakeUp() {
        awake = true;
        System.out.println(name + " wakes up.");
    }

    public void eat(String food) {
        if (!awake) System.out.println(name + " is asleep!");
        else System.out.println(name + " eats " + food + ".");
    }

    public String getName()               { return name;                }
    public int    getLifeExpectancy()     { return lifeExpectancyYears; }
    public boolean isAwake()              { return awake;               }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "[" + name + "]";
    }
}

// ── Four independent subclasses — each specialises differently ─────────
public class Dog extends Animal {
    private final String breed;
    public Dog(String name, String breed) {
        super(name, 12);
        this.breed = breed;
    }
    @Override public String makeSound() { return "Woof!";          }
    @Override public String move()      { return "runs on 4 legs"; }
    @Override public String diet()      { return "omnivore";       }
    public void fetch()                 { System.out.println(getName() + " fetches!"); }
    public String getBreed()            { return breed; }
}

public class Cat extends Animal {
    private boolean indoor;
    public Cat(String name, boolean indoor) {
        super(name, 15);
        this.indoor = indoor;
    }
    @Override public String makeSound() { return "Meow!";          }
    @Override public String move()      { return "stalks silently"; }
    @Override public String diet()      { return "carnivore";       }
    public void purr()                  { System.out.println(getName() + " purrs."); }
    public boolean isIndoor()           { return indoor; }
}

public class Bird extends Animal {
    private final double wingspanCm;
    public Bird(String name, double wingspanCm) {
        super(name, 8);
        this.wingspanCm = wingspanCm;
    }
    @Override public String makeSound() { return "Tweet!";     }
    @Override public String move()      { return "flies";      }
    @Override public String diet()      { return "seed eater"; }
    public void sing()                  { System.out.println(getName() + " sings melodiously."); }
    public double getWingspan()         { return wingspanCm; }
}

public class Fish extends Animal {
    private final boolean saltwater;
    public Fish(String name, boolean saltwater) {
        super(name, 5);
        this.saltwater = saltwater;
    }
    @Override public String makeSound() { return "(silent)";    }
    @Override public String move()      { return "swims";       }
    @Override public String diet()      { return "plankton";    }
    public boolean isSaltwater()        { return saltwater; }
}

Polymorphism Maximised

Hierarchical inheritance maximises the value of polymorphism. A single Animal reference can point to any of the subclasses. Code written to work with Animal works correctly with Dog, Cat, Bird, Fish, and any future Animal subclass added to the program — without any change to the existing code. This is the Open/Closed Principle in action: the code is open for extension (new subclasses can be added) and closed for modification (existing code does not need to change when they are). A List<Animal> can contain all four species together. Iterating the list and calling makeSound() on each invokes Dog's Woof!, Cat's Meow!, Bird's Tweet!, and Fish's silence — the right implementation for each type, automatically, through dynamic dispatch. This is the power of a well-designed superclass: it defines a common vocabulary (a set of operations) that allows all subclasses to be used uniformly. The real test of a hierarchical design's quality is whether new subclasses can be added without modifying any existing code. If adding a new species (Horse extends Animal) requires changing switch statements, instanceof chains, or factory methods scattered through the codebase, the polymorphism is incomplete — behaviour that belongs inside the class has leaked out. When new subclasses slot in cleanly and all existing code handles them automatically, the design is working as intended.
Java
// ── Polymorphic collection — all four types treated uniformly ────────
List<Animal> zoo = new ArrayList<>();
zoo.add(new Dog("Rex",     "Labrador"));
zoo.add(new Cat("Whiskers", true));
zoo.add(new Bird("Tweety",  25.0));
zoo.add(new Fish("Nemo",    true));

// ── All animals respond to the same operations ────────────────────────
System.out.println("=== Morning Routine ===");
for (Animal animal : zoo) {
    animal.wakeUp();
    System.out.println(animal.getName() + " says: " + animal.makeSound());
    System.out.println(animal.getName() + " " + animal.move());
    animal.eat(animal.diet());
    System.out.println();
}

// ── Aggregation works across all types ───────────────────────────────
double avgLifeExpectancy = zoo.stream()
    .mapToInt(Animal::getLifeExpectancy)
    .average()
    .orElse(0);
System.out.println("Average life expectancy: " + avgLifeExpectancy);

// ── Open/Closed — add Horse without changing any existing code ────────
public class Horse extends Animal {
    private final String colour;
    public Horse(String name, String colour) {
        super(name, 25);
        this.colour = colour;
    }
    @Override public String makeSound() { return "Neigh!";      }
    @Override public String move()      { return "gallops";     }
    @Override public String diet()      { return "herbivore";   }
    public void gallop()                { System.out.println(getName() + " gallops!"); }
}

zoo.add(new Horse("Spirit", "palomino"));
// The loop above now handles Horse automatically — zero changes to loop
// This is the Open/Closed Principle: open for extension, closed for modification

// ── Pattern matching and type-specific operations ─────────────────────
for (Animal animal : zoo) {
    // General Animal operations first
    System.out.print(animal + ": " + animal.makeSound() + " ");

    // Type-specific operations only when needed
    if (animal instanceof Dog dog)   dog.fetch();
    else if (animal instanceof Cat cat) cat.purr();
    else if (animal instanceof Bird bird) bird.sing();
    else System.out.println();
}

Abstract Classes as Superclass

An abstract class is the ideal form for a superclass in a hierarchical design. The abstract keyword prevents direct instantiation — no one can write new Animal() — which enforces the design intent that Animal exists only as a base for concrete subclasses. Abstract classes can declare abstract methods — methods with no body that every concrete subclass must implement. They can also provide concrete methods with implementations shared by all subclasses. The combination of abstract and concrete methods in an abstract class defines the template for subclass behaviour. Abstract methods are the variation points — each subclass fills them in with its specific behaviour. Concrete methods are the fixed framework — the logic that is the same for all subclasses and need not be duplicated. This is the template method pattern: the abstract class defines a skeleton algorithm in a concrete method, and calls abstract methods (hook methods) that subclasses override to customise the steps. Abstract classes occupy the middle ground between interfaces and concrete classes. Like interfaces, they cannot be instantiated and define an API that subclasses must implement. Like concrete classes, they can have fields, constructors, and implemented methods. This combination makes them the right tool when subclasses share some common state and some common behaviour, but also vary in specific ways.
Java
// ── Abstract class as hierarchical superclass ────────────────────────
public abstract class Shape {

    private String     colour;
    private boolean    filled;
    private static int instanceCount = 0;

    // ── Protected constructor — only subclasses call it ────────────────
    protected Shape(String colour, boolean filled) {
        this.colour  = colour;
        this.filled  = filled;
        instanceCount++;
    }

    // ── Abstract methods — subclass MUST provide these ────────────────
    public abstract double area();
    public abstract double perimeter();
    public abstract String shapeName();

    // ── Concrete method — common to ALL shapes ────────────────────────
    public String describe() {
        return String.format(
            "%s (colour=%s, filled=%b, area=%.2f, perimeter=%.2f)",
            shapeName(), colour, filled, area(), perimeter());
    }

    // ── Template method — fixed algorithm, variable steps ─────────────
    public final void printReport() {
        System.out.println("=== Shape Report ===");
        System.out.println("Type:      " + shapeName());
        System.out.println("Colour:    " + colour);
        System.out.println("Filled:    " + filled);
        System.out.printf( "Area:      %.4f%n",      area());
        System.out.printf( "Perimeter: %.4f%n",      perimeter());
        printAdditionalDetails();   // ← hook — subclass overrides this
        System.out.println("====================");
    }

    // ── Hook method — subclasses override to add type-specific details ─
    protected void printAdditionalDetails() {
        // Default: nothing. Subclasses override when they have more to add.
    }

    public String  getColour()              { return colour;         }
    public boolean isFilled()               { return filled;         }
    public void    setColour(String c)      { this.colour = c;       }
    public void    setFilled(boolean f)     { this.filled = f;       }
    public static  int getInstanceCount()   { return instanceCount;  }

    // ── Cannot instantiate abstract class ─────────────────────────────
    // new Shape("red", true);   // COMPILE ERROR — Shape is abstract
}

// ── Concrete subclasses ────────────────────────────────────────────────
public class Circle extends Shape {

    private final double radius;

    public Circle(double radius, String colour, boolean filled) {
        super(colour, filled);
        if (radius <= 0) throw new IllegalArgumentException(
            "Radius must be positive");
        this.radius = radius;
    }

    @Override public String shapeName()   { return "Circle";               }
    @Override public double area()        { return Math.PI * radius * radius; }
    @Override public double perimeter()   { return 2 * Math.PI * radius;   }
    public double getRadius()             { return radius; }

    @Override
    protected void printAdditionalDetails() {
        System.out.printf("Radius:    %.4f%n", radius);
        System.out.printf("Diameter:  %.4f%n", radius * 2);
    }
}

public class Rectangle extends Shape {

    private final double width;
    private final double height;

    public Rectangle(double width, double height,
                     String colour, boolean filled) {
        super(colour, filled);
        this.width  = width;
        this.height = height;
    }

    @Override public String shapeName()   { return "Rectangle";          }
    @Override public double area()        { return width * height;        }
    @Override public double perimeter()   { return 2 * (width + height);  }

    public boolean isSquare() { return width == height; }

    @Override
    protected void printAdditionalDetails() {
        System.out.printf("Width:     %.4f%n", width);
        System.out.printf("Height:    %.4f%n", height);
        System.out.println("Square:    " + isSquare());
    }
}

public class Triangle extends Shape {

    private final double sideA, sideB, sideC;

    public Triangle(double a, double b, double c,
                    String colour, boolean filled) {
        super(colour, filled);
        if (a + b <= c || a + c <= b || b + c <= a)
            throw new IllegalArgumentException("Invalid triangle sides");
        this.sideA = a; this.sideB = b; this.sideC = c;
    }

    @Override public String shapeName() { return "Triangle"; }
    @Override public double perimeter() { return sideA + sideB + sideC; }
    @Override public double area() {
        double s = perimeter() / 2;         // Heron's formula
        return Math.sqrt(s*(s-sideA)*(s-sideB)*(s-sideC));
    }
}

// ── Template method — each subclass customises the report ─────────────
new Circle(5.0, "red", true).printReport();
new Rectangle(4.0, 6.0, "blue", false).printReport();

Sibling Independence and Avoiding Cross-Coupling

A critical property of hierarchical inheritance is that sibling classes are completely independent. Dog does not know Cat exists; Circle does not know Rectangle exists. This independence is architecturally valuable — adding a new subclass (Horse, Ellipse) requires only writing the new class and optionally registering it in factory methods or configuration. No sibling class ever needs to be changed. This independence must be maintained by design. The moment one sibling class refers to another, the hierarchy gains unwanted coupling. Code that instanceof-checks sibling types or casts between siblings is a smell that the design has broken down. If a Dog-specific operation needs to happen when a Cat is also present, that logic probably belongs in a class that owns both — the container, the scenario, the service — not in the sibling classes themselves. The instanceof cascade over siblings is the most common violation: if (animal instanceof Dog) ... else if (animal instanceof Cat) ... else if (animal instanceof Bird). Every time a new subclass is added, this cascade must be updated. The correct design pushes the varying behaviour into each subclass as a polymorphic method, eliminating the cascade entirely. If Cat and Dog genuinely need different treatment, that difference should be expressed through methods in the Animal API that Cat and Dog implement differently.
Java
// ── Sibling coupling — design smell ──────────────────────────────────
// WRONG: one sibling knows about another
public class Dog extends Animal {

    // Dog should NOT know Cat exists
    public void interact(Animal other) {
        if (other instanceof Cat cat) {      // coupled to sibling
            System.out.println(getName() + " chases " + cat.getName());
        } else if (other instanceof Dog dog) {
            System.out.println(getName() + " plays with " + dog.getName());
        }
        // Must update this code every time a new Animal subclass is added
    }
}

// ── Better: polymorphic interaction through the Animal API ─────────────
public abstract class Animal {

    // ...existing methods...

    // Each subclass defines how it reacts to meeting another animal
    public abstract String reactionTo(Animal other);

    // Template: uses the polymorphic reaction
    public void meet(Animal other) {
        System.out.println(getName() + " meets " + other.getName() + ": "
            + reactionTo(other));
    }
}

public class Dog extends Animal {
    @Override
    public String reactionTo(Animal other) {
        // Dog's reaction — no knowledge of other specific types
        if (other instanceof Dog) return "wags tail excitedly";
        return "sniffs cautiously";  // generic reaction to unknown animal
    }
}

public class Cat extends Animal {
    @Override
    public String reactionTo(Animal other) {
        if (other instanceof Cat) return "hisses territorially";
        return "flicks tail and watches";
    }
}

// Adding a new Horse subclass: just define Horse.reactionTo()
// Existing Dog and Cat code does NOT change — zero coupling to Horse

// ── Eliminating instanceof cascades with polymorphism ─────────────────
// WRONG — cascade over siblings, breaks with every new subclass:
double totalSound(List<Animal> animals) {
    double total = 0;
    for (Animal a : animals) {
        if      (a instanceof Dog)  total += 80;   // dB estimate
        else if (a instanceof Cat)  total += 60;
        else if (a instanceof Bird) total += 70;
        // Must add a line for every new Animal subclass
    }
    return total;
}

// CORRECT — polymorphism, never needs to change:
public abstract class Animal {
    public abstract int soundLevelDb();   // each subclass implements
}

public class Dog  extends Animal { @Override public int soundLevelDb() { return 80; } }
public class Cat  extends Animal { @Override public int soundLevelDb() { return 60; } }
public class Bird extends Animal { @Override public int soundLevelDb() { return 70; } }

double totalSound(List<Animal> animals) {
    return animals.stream()
        .mapToInt(Animal::soundLevelDb)   // no instanceof, no coupling
        .sum();
}
// Adding Horse extends Animal with soundLevelDb() = 85 — totalSound still works

Hierarchical Inheritance with Interfaces

Java's restriction to single class inheritance means that hierarchical subclasses can independently implement interfaces while still sharing the superclass. This is the mechanism for adding cross-cutting capabilities — serialisation, comparability, formatting — to selected subclasses without forcing them on all siblings. A Circle can implement Comparable<Circle> without forcing Cat to be Comparable. A Dog can implement Serializable without affecting Fish. Interfaces at the subclass level create a richer type hierarchy. A variable typed as Comparable can hold any Circle or any Comparable type. A variable typed as Animal can hold any animal. The same Dog object satisfies both. This multiple type membership through interfaces complements the single-chain class hierarchy — classes inherit implementation from one source (the superclass chain) but commit to multiple contracts (the interfaces). The interaction between hierarchical inheritance and interfaces is where much of Java's expressive power lies. An abstract class defines what all subclasses are; interfaces define additional capabilities specific subclasses have. The abstract class captures the is-a relationship; the interfaces capture can-do relationships.
Java
// ── Subclasses implement interfaces independently ─────────────────────
public abstract class Animal {
    protected String name;
    protected Animal(String name) { this.name = name; }
    public abstract String makeSound();
    public String getName() { return name; }
}

// ── Dog: Trainable and Comparable (specific to Dog) ───────────────────
public class Dog extends Animal
        implements Comparable<Dog>, Cloneable {

    private int trainingLevel;   // 0-10

    public Dog(String name, int trainingLevel) {
        super(name);
        this.trainingLevel = trainingLevel;
    }

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

    @Override
    public int compareTo(Dog other) {
        return Integer.compare(this.trainingLevel, other.trainingLevel);
    }

    @Override
    public Dog clone() {
        try { return (Dog) super.clone(); }
        catch (CloneNotSupportedException e) {
            throw new AssertionError("Cloneable but cannot clone");
        }
    }

    public int getTrainingLevel() { return trainingLevel; }
}

// ── Bird: only Comparable, not Cloneable ────────────────────────────
public class Bird extends Animal implements Comparable<Bird> {

    private double wingspanCm;

    public Bird(String name, double wingspanCm) {
        super(name);
        this.wingspanCm = wingspanCm;
    }

    @Override public String makeSound() { return "Tweet"; }

    @Override
    public int compareTo(Bird other) {
        return Double.compare(this.wingspanCm, other.wingspanCm);
    }

    public double getWingspan() { return wingspanCm; }
}

// ── Cat: neither Comparable nor Cloneable ────────────────────────────
public class Cat extends Animal {
    public Cat(String name) { super(name); }
    @Override public String makeSound() { return "Meow"; }
}

// ── Multiple type memberships ─────────────────────────────────────────
Dog rex   = new Dog("Rex",    8);
Dog buddy = new Dog("Buddy",  5);
Bird eagle = new Bird("Eagle", 200.0);

// Used as Animal
List<Animal> animals = List.of(rex, buddy, eagle, new Cat("Whiskers"));
animals.forEach(a -> System.out.println(a.getName() + ": " + a.makeSound()));

// Used as Comparable<Dog>
List<Dog> dogs = new ArrayList<>(List.of(rex, buddy,
    new Dog("Max", 9)));
Collections.sort(dogs);   // sorted by training level
dogs.forEach(d -> System.out.println(d.getName() + "=" + d.getTrainingLevel()));

// Dog cloned — Cat cannot be cloned
Dog rexClone = rex.clone();
System.out.println(rexClone == rex);   // false — different objects
System.out.println(rexClone.getName()); // Rex — same name

Related Topics in Object-Oriented Programming

OOP Concepts
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects — self-contained units that combine data (fields) and behaviour (methods). Java is a class-based, object-oriented language where almost everything is an object. OOP provides four foundational principles: encapsulation, inheritance, polymorphism, and abstraction. Together they produce software that is modular, reusable, maintainable, and easier to reason about as systems grow in complexity.
Classes
A class is the fundamental building block of Java. It is a blueprint that defines the structure and behaviour of objects — what data each object holds (fields) and what operations it can perform (methods). Every Java program is composed of classes. Understanding how to design a class well — choosing the right access modifiers, separating state from behaviour, and writing cohesive single-responsibility classes — is the foundation of object-oriented programming in Java. This entry covers class anatomy, fields, methods, access modifiers, static vs instance members, the this keyword, and class design principles.
Objects
An object is a runtime instance of a class. Where a class is a blueprint that exists in source code, an object is a living entity that exists in memory during program execution — it has its own identity, its own state stored in its fields, and the ability to respond to method calls. Every object has three fundamental properties: identity (a unique memory address), state (the current values of its fields), and behaviour (the methods it responds to). This entry covers object identity vs equality, the Object class hierarchy, object state and mutation, method calls, toString, equals and hashCode, and the object lifecycle.
Object Creation
Object creation in Java is the process of allocating memory, initialising fields, and running constructor logic to bring an object into existence. The new keyword is the primary mechanism, but Java also provides factory methods, builder patterns, copy constructors, and object cloning. Constructors are special methods that set up the initial state — their design determines how easy or difficult the class is to use correctly. This entry covers constructors in depth, constructor overloading and chaining, copy constructors, factory methods, the builder pattern, and the difference between shallow and deep copy.