β˜• Java

Java Inheritance β€” extends, super, Overriding & Best Practices

Everything you need to know about Java Inheritance β€” types of inheritance, extends keyword, super keyword, method overriding, constructor chaining, final keyword, IS-A vs HAS-A, instanceof, Liskov Substitution Principle, anti-patterns, and real-world production code examples.

πŸ“…

Last Updated

March 2026

⏱️

Read Time

27 min

🎯

Level

Beginner to Intermediate

🏷️

Chapter

19 of 35

What is Inheritance in Java?

Inheritance is one of the four pillars of object-oriented programming (alongside Encapsulation, Polymorphism, and Abstraction). It is the mechanism by which one class β€” called the subclass or child class β€” acquires the fields and methods of another class β€” called the superclass or parent class. In Java, inheritance is declared using the extends keyword.

Inheritance models the IS-A relationship between classes. A Dog IS-A Animal. A SavingsAccount IS-A BankAccount. A ElectricCar IS-A Vehicle. When this relationship holds, the subclass can reuse the parent's code, extend it with new behaviour, and substitute for the parent wherever the parent type is expected β€” this is the essence of runtime polymorphism.

The two core benefits of inheritance are: (1) Code Reuse β€” common logic written once in the parent is inherited by all subclasses without duplication. (2) Polymorphism β€” a parent type reference can hold any subclass object, and calling a method on it invokes the subclass's version at runtime. This enables writing flexible, extensible systems where new subclasses can be added without modifying existing code.

The extends Keyword β€” Establishing Inheritance

The extends keyword is Java's mechanism for declaring that one class inherits from another. It creates a permanent parent-child relationship between the two classes. The subclass automatically receives all non-private members of the superclass and can also define its own additional fields and methods.

πŸ“Œ
Syntax

public class SubClass extends SuperClass { // Inherited: all public and protected members of SuperClass // Can add: new fields, new methods // Can override: non-private, non-final methods // Cannot override: private methods, static methods (hidden, not overridden) }

πŸ”’
What Gets Inherited

βœ… Inherited: public fields, protected fields, public methods, protected methods, default (package-private) members (same package only). ❌ NOT inherited: private fields and methods (hidden within declaring class), constructors (but accessible via super()), static members (accessible but not 'inherited' in polymorphic sense).

🧬
Single Inheritance Rule

A Java class can extend ONLY ONE class β€” this is single inheritance. 'class C extends A, B' is a compile error. This avoids the Diamond Problem. However, a class CAN implement multiple interfaces: 'class C extends A implements B, D'. And an interface CAN extend multiple interfaces: 'interface C extends A, B'.

β˜• JavaExtendsKeywordDemo.java
// ── PARENT CLASS ──────────────────────────────────────
public class Vehicle {

    // protected β€” accessible in subclasses across packages
    protected String brand;
    protected String model;
    protected int    year;
    protected double fuelLevel; // 0.0 to 1.0

    // private β€” NOT inherited by subclasses
    private String vehicleIdentificationNumber;

    public Vehicle(String brand, String model, int year) {
        this.brand  = brand;
        this.model  = model;
        this.year   = year;
        this.fuelLevel = 1.0;
        this.vehicleIdentificationNumber = generateVIN();
    }

    // public β€” inherited and can be overridden
    public void startEngine() {
        System.out.println(brand + " " + model + " engine started.");
    }

    public void refuel(double amount) {
        fuelLevel = Math.min(1.0, fuelLevel + amount);
        System.out.printf("Refuelled. Fuel level: %.0f%%%n", fuelLevel * 100);
    }

    public String getDetails() {
        return String.format("%s %s (%d) | Fuel: %.0f%%",
               brand, model, year, fuelLevel * 100);
    }

    // private β€” NOT accessible in subclasses
    private String generateVIN() {
        return "VIN-" + System.currentTimeMillis();
    }
}

// ── SUBCLASS β€” inherits Vehicle ───────────────────────
public class Car extends Vehicle {

    private int   numberOfDoors;
    private String transmissionType; // "Manual" or "Automatic"

    public Car(String brand, String model, int year,
               int doors, String transmission) {
        super(brand, model, year); // βœ… Must call parent constructor
        this.numberOfDoors    = doors;
        this.transmissionType = transmission;
    }

    // NEW method β€” only in Car
    public void openTrunk() {
        System.out.println(model + " trunk opened.");
    }

    // OVERRIDING parent method β€” custom behaviour for Car
    @Override
    public String getDetails() {
        return super.getDetails() +  // Reuse parent's version
               String.format(" | Doors: %d | %s",
                             numberOfDoors, transmissionType);
    }
}

// ── ANOTHER SUBCLASS ──────────────────────────────────
public class Truck extends Vehicle {

    private double payloadCapacityTons;

    public Truck(String brand, String model, int year, double payload) {
        super(brand, model, year);
        this.payloadCapacityTons = payload;
    }

    @Override
    public void startEngine() {
        System.out.println(brand + " " + model +
                           " heavy engine roaring to life!");
    }

    public double getPayload() { return payloadCapacityTons; }
}

// ── USAGE ─────────────────────────────────────────────
// Car    car   = new Car("Toyota", "Camry", 2024, 4, "Automatic");
// Truck  truck = new Truck("Tata", "Prima", 2023, 25.0);
// car.startEngine();  // Toyota Camry engine started.
// truck.startEngine(); // Tata Prima heavy engine roaring to life!
// car.openTrunk();    // Camry trunk opened.
// System.out.println(car.getDetails());
// β†’ Toyota Camry (2024) | Fuel: 100% | Doors: 4 | Automatic

Types of Inheritance in Java

Java supports several forms of inheritance, each modelling a different structural relationship between classes. Understanding these types helps you design class hierarchies that are clean, logical, and extensible.

1️⃣
Single Inheritance

One class extends exactly ONE parent class. The most common and straightforward form. Example: 'class Car extends Vehicle'. Car inherits all accessible members of Vehicle. Java mandates single inheritance to avoid the Diamond Problem. This is the default and most-used inheritance type in Java applications.

πŸ“Ά
Multilevel Inheritance

A class extends another class which itself extends another class β€” forming a chain. Example: Animal β†’ Mammal β†’ Dog. Dog inherits from both Mammal and Animal. The chain can be as long as needed, but deep chains (4+ levels) are generally a code smell β€” they make the hierarchy fragile and hard to understand. Rule: keep hierarchies shallow.

🌿
Hierarchical Inheritance

Multiple classes extend the SAME parent class. Example: Vehicle is extended by Car, Truck, and Motorcycle. All three inherit the common Vehicle behaviour but add their own specialisations. This is the most natural use of inheritance β€” capturing shared structure in a parent and specialising in children.

πŸ”€
Multiple Inheritance β€” Through Interfaces Only

Java does NOT allow a class to extend multiple classes. However, a class CAN implement multiple interfaces: 'class Robot implements Movable, Chargeable, Programmable'. And an interface CAN extend multiple interfaces. Java 8+ default methods in interfaces can provide shared implementations, giving most benefits of multiple inheritance without the Diamond Problem.

🚫
Hybrid Inheritance β€” Not Directly Supported

A combination of multiple inheritance types. Java does not support hybrid inheritance through classes for the same reason it avoids multiple class inheritance. However, hybrid-like structures are achievable using interfaces and abstract classes together β€” a common real-world pattern in enterprise frameworks like Spring.

β˜• JavaTypesOfInheritance.java
// ── SINGLE INHERITANCE ────────────────────────────────
class Animal {
    String name;
    void breathe() { System.out.println(name + " breathes."); }
}
class Dog extends Animal {         // Single β€” Dog extends Animal
    void bark() { System.out.println(name + " barks!"); }
}

// ── MULTILEVEL INHERITANCE ────────────────────────────
class LivingBeing {
    void eat() { System.out.println("Eating..."); }
}
class Animal2 extends LivingBeing {  // Level 1
    void breathe() { System.out.println("Breathing..."); }
}
class Dog2 extends Animal2 {          // Level 2 β€” inherits from both
    void bark() { System.out.println("Barking!"); }
}
// Dog2 object can call: eat(), breathe(), bark()

// ── HIERARCHICAL INHERITANCE ──────────────────────────
class Shape {
    String colour;
    double getArea() { return 0; }
}
class Circle    extends Shape {  // Child 1
    double radius;
    @Override double getArea() { return Math.PI * radius * radius; }
}
class Rectangle extends Shape {  // Child 2
    double width, height;
    @Override double getArea() { return width * height; }
}
class Triangle  extends Shape {  // Child 3
    double base, height;
    @Override double getArea() { return 0.5 * base * height; }
}

// ── MULTIPLE INHERITANCE via INTERFACES ───────────────
interface Flyable  { void fly(); }
interface Swimmable { void swim(); }

class Duck extends Animal implements Flyable, Swimmable {
    @Override public void fly()  { System.out.println(name + " flies."); }
    @Override public void swim() { System.out.println(name + " swims."); }
}
// Duck IS-A Animal, IS-A Flyable, IS-A Swimmable β€” all valid!

// ❌ NOT ALLOWED β€” multiple class inheritance
// class Mule extends Horse, Donkey { } // Compile error

The super Keyword β€” Accessing the Parent

The super keyword in Java is a reference to the immediate parent class. It is the complement to this β€” where this refers to the current object, super refers to the parent portion of the current object. It has three distinct uses, each critical to working correctly with inheritance.

1️⃣
super.field β€” Access Hidden Parent Field

When a subclass declares a field with the same name as a parent field, the parent's field is 'hidden'. 'super.fieldName' accesses the parent's version. This is rarely needed in well-designed code β€” field hiding is generally a bad practice. Prefer unique field names in subclasses. But understanding this is essential for exam questions.

2️⃣
super.method() β€” Call Overridden Parent Method

When a subclass overrides a parent method, 'super.method()' calls the parent's original version. This is very common β€” a subclass often wants to EXTEND rather than REPLACE the parent's behaviour. Example: getDetails() in Car calls 'super.getDetails()' to include the parent's output, then appends its own. Without super, the parent logic must be duplicated.

3️⃣
super() β€” Call Parent Constructor

super(args) calls the parent's constructor. MUST be the FIRST statement in the subclass constructor. If not written explicitly, Java auto-inserts 'super()' (no-arg parent constructor). If the parent has no no-arg constructor, omitting super(args) is a compile error. Used to initialise the inherited portion of the object β€” the parent's fields need to be set up before the subclass extends them.

β˜• JavaSuperKeywordDemo.java
public class Employee {

    protected String name;
    protected String department;
    protected double baseSalary;

    public Employee(String name, String department, double baseSalary) {
        this.name       = name;
        this.department = department;
        this.baseSalary = baseSalary;
    }

    public double calculateMonthlyPay() {
        return baseSalary;
    }

    public String getPaySlip() {
        return String.format("Employee: %s | Dept: %s | Base: β‚Ή%.2f",
                            name, department, baseSalary);
    }
}

// ── Manager EXTENDS Employee ──────────────────────────
public class Manager extends Employee {

    private double teamBonus;
    private int    teamSize;

    public Manager(String name, String department,
                   double baseSalary, double teamBonus, int teamSize) {
        super(name, department, baseSalary); // βœ… Use 1: calls Employee constructor
        this.teamBonus = teamBonus;
        this.teamSize  = teamSize;
    }

    // βœ… Use 2: call overridden method to extend (not replace) logic
    @Override
    public double calculateMonthlyPay() {
        double base      = super.calculateMonthlyPay(); // Parent's version
        double bonusShare = teamBonus / teamSize;       // Manager's extra
        return base + bonusShare;
    }

    @Override
    public String getPaySlip() {
        return super.getPaySlip() +  // βœ… Reuse parent's formatted string
               String.format(" | Bonus Share: β‚Ή%.2f | Team: %d",
                             teamBonus / teamSize, teamSize);
    }
}

// ── SeniorManager EXTENDS Manager (Multilevel) ────────
public class SeniorManager extends Manager {

    private double performanceBonus;

    public SeniorManager(String name, String department,
                         double baseSalary, double teamBonus,
                         int teamSize, double perfBonus) {
        super(name, department, baseSalary, teamBonus, teamSize);
        this.performanceBonus = perfBonus;
    }

    @Override
    public double calculateMonthlyPay() {
        return super.calculateMonthlyPay() + performanceBonus;
    }

    @Override
    public String getPaySlip() {
        return super.getPaySlip() +
               String.format(" | Perf Bonus: β‚Ή%.2f", performanceBonus);
    }
}

// Usage:
// Employee   e = new Employee("Ravi",   "Dev",    60000);
// Manager    m = new Manager("Priya",   "Dev",    90000, 50000, 5);
// SeniorManager s = new SeniorManager("Arjun", "Dev", 130000, 80000, 8, 20000);
// System.out.println(e.getPaySlip());
// System.out.println(m.getPaySlip());
// System.out.println(s.getPaySlip());

Constructor Chaining in Inheritance

When an object is created from a subclass, Java does not just call the subclass constructor. It initiates a constructor chain β€” walking up the entire inheritance hierarchy and executing each class's constructor from the top (Object) downward. This ensures every level of the hierarchy has a chance to initialise its own fields before the subclass adds to them.

πŸ“‹
Constructor Chain Order

When 'new GrandChild()' is called, execution order is: 1. GrandChild() starts, immediately calls super() 2. Child() starts, immediately calls super() 3. Object() runs (java.lang.Object constructor) 4. Child() body continues 5. GrandChild() body continues Each constructor's body runs from TOP (most ancestral) to BOTTOM (most derived). Fields are always initialised before the constructor body they belong to runs.

⚠️
The Auto-insert Rule

If a subclass constructor does NOT explicitly call super() or this() as its first statement, Java automatically inserts 'super()' (no-arg parent constructor) at the very beginning. Consequence: if the parent class has NO no-arg constructor (because a parameterised one was defined), the compile fails with 'no suitable constructor found'. Always explicitly call super(args) when the parent has no default constructor.

πŸ”—
this() vs super() β€” Cannot Have Both

Both 'this(args)' (same-class constructor chaining) and 'super(args)' (parent constructor) must be the FIRST statement in a constructor. Therefore, a single constructor CANNOT have both. The way it works: 'this(args)' delegates to another constructor in the same class, which eventually calls super(). The super() call bubbles up through 'this()' chains, not directly.

β˜• JavaConstructorChainingDemo.java
class A {
    A() {
        System.out.println("A constructor");
    }
}

class B extends A {
    B() {
        // Java auto-inserts: super(); here if not written
        System.out.println("B constructor");
    }
}

class C extends B {
    C() {
        // Java auto-inserts: super(); here if not written
        System.out.println("C constructor");
    }
}

// new C() output:
// A constructor   ← runs first (top of chain)
// B constructor
// C constructor   ← runs last (most derived)

// ── PRACTICAL EXAMPLE ─────────────────────────────────
class Account {
    protected String accountId;
    protected String ownerName;
    protected double balance;

    Account(String accountId, String ownerName, double initialDeposit) {
        if (initialDeposit < 0)
            throw new IllegalArgumentException("Initial deposit cannot be negative");
        this.accountId  = accountId;
        this.ownerName  = ownerName;
        this.balance    = initialDeposit;
        System.out.println("Account created: " + accountId);
    }
}

class SavingsAccount extends Account {
    private double interestRate;
    private double minimumBalance;

    SavingsAccount(String id, String owner,
                   double deposit, double interestRate) {
        super(id, owner, deposit);          // βœ… Initialise Account part first
        this.interestRate   = interestRate;
        this.minimumBalance = 1000.0;
        System.out.println("SavingsAccount configured. Rate: " + interestRate);
    }

    double calculateYearlyInterest() {
        return balance * interestRate / 100;
    }
}

class FixedDepositAccount extends SavingsAccount {
    private int    tenureMonths;
    private double lockinPenaltyPercent;

    FixedDepositAccount(String id, String owner, double deposit,
                        double rate, int tenure) {
        super(id, owner, deposit, rate);     // βœ… Initialise SavingsAccount part
        this.tenureMonths          = tenure;
        this.lockinPenaltyPercent  = 1.5;
        System.out.println("FD Account. Tenure: " + tenure + " months");
    }
}

// new FixedDepositAccount("FD001","Ravi",100000,7.5,24) prints:
// Account created: FD001
// SavingsAccount configured. Rate: 7.5
// FD Account. Tenure: 24 months

Method Overriding β€” Runtime Polymorphism

Method overriding occurs when a subclass provides its own implementation for a method that is already defined in its parent class. It is the mechanism behind runtime polymorphism (dynamic dispatch) β€” when you call a method on a parent-type reference that actually holds a subclass object, Java calls the subclass's version at runtime, not the parent's.

βœ…
Rules for Valid Overriding

1. Same method name as parent. 2. Same parameter list (number, types, order). 3. Return type: same, or a covariant (subtype) return. 4. Access modifier: same or WIDER (protectedβ†’public βœ…, publicβ†’protected ❌). 5. Cannot override: private methods (not visible), static methods (hiding, not overriding), final methods. 6. @Override annotation: not mandatory but strongly recommended β€” compiler catches mistakes.

πŸ”„
Dynamic Dispatch β€” How Runtime Polymorphism Works

When 'Animal a = new Dog(); a.makeSound();' is written, Java doesn't decide which makeSound() to call at compile time β€” it decides at RUNTIME by looking at the actual object type (Dog), not the reference type (Animal). This is dynamic dispatch. The JVM maintains a virtual method table (vtable) for each class β€” method calls are resolved through this table at runtime. This is how 'write once, work with many types' polymorphism works.

πŸ“Œ
@Override Annotation

@Override tells the compiler: 'I intend this to be an override'. The compiler then verifies it actually overrides a parent method. Benefits: (1) Compile error if method name is misspelled. (2) Compile error if parameter list doesn't match. (3) Documents intent clearly. Without @Override, a typo in the method name silently creates a NEW method instead of overriding β€” a hard-to-find bug.

β˜• JavaMethodOverridingDemo.java
// ── PARENT ────────────────────────────────────────────
public abstract class PaymentProcessor {

    protected String processorName;
    protected double transactionFeePercent;

    public PaymentProcessor(String name, double fee) {
        this.processorName        = name;
        this.transactionFeePercent = fee;
    }

    // Template method β€” calls overridable processTransaction()
    public final PaymentResult pay(double amount, String currency) {
        if (amount <= 0)
            return PaymentResult.failure("Amount must be positive");
        double fee    = amount * transactionFeePercent / 100;
        double netAmt = amount - fee;
        System.out.printf("[%s] Processing %.2f %s (fee: %.2f)%n",
                          processorName, amount, currency, fee);
        return processTransaction(netAmt, currency); // Calls overridden version
    }

    // ← Subclasses MUST override this to provide gateway-specific logic
    protected abstract PaymentResult processTransaction(double amount, String currency);

    // Can be overridden if needed
    public String getProcessorInfo() {
        return processorName + " (Fee: " + transactionFeePercent + "%)";
    }
}

// ── SUBCLASS 1: UPI ───────────────────────────────────
public class UPIProcessor extends PaymentProcessor {

    private String upiId;

    public UPIProcessor(String upiId) {
        super("UPI", 0.0); // UPI has zero transaction fee
        this.upiId = upiId;
    }

    @Override
    protected PaymentResult processTransaction(double amount, String currency) {
        System.out.println("  β†’ UPI transfer to " + upiId);
        return PaymentResult.success("UPI-" + System.currentTimeMillis());
    }

    @Override
    public String getProcessorInfo() {
        return super.getProcessorInfo() + " | UPI ID: " + upiId;
    }
}

// ── SUBCLASS 2: Credit Card ───────────────────────────
public class CreditCardProcessor extends PaymentProcessor {

    private String maskedCardNumber;

    public CreditCardProcessor(String maskedCard) {
        super("CreditCard", 1.8); // 1.8% fee
        this.maskedCardNumber = maskedCard;
    }

    @Override
    protected PaymentResult processTransaction(double amount, String currency) {
        System.out.println("  β†’ Credit card charge on " + maskedCardNumber);
        return PaymentResult.success("CC-" + System.currentTimeMillis());
    }
}

// ── RUNTIME POLYMORPHISM IN ACTION ────────────────────
// PaymentProcessor processor; // Parent type reference
//
// processor = new UPIProcessor("ravi@upi");
// processor.pay(1000, "INR"); // Calls UPIProcessor.processTransaction()
//
// processor = new CreditCardProcessor("**** **** 4521");
// processor.pay(5000, "INR"); // Calls CreditCardProcessor.processTransaction()
//
// SAME method call 'processor.pay()' β€” DIFFERENT behaviour at runtime!

final with Inheritance β€” Preventing Extension & Override

The final keyword interacts with inheritance in three important ways β€” preventing subclassing, preventing method override, and making fields immutable. These restrictions are deliberate design decisions that protect invariants and communicate intent.

ContextWhat final PreventsExampleCommon Use Case
final classCannot be subclassed β€” no extends allowedpublic final class StringSecurity-sensitive or complete implementations: String, Integer, Math
final methodCannot be overridden in any subclasspublic final void processPayment()Template method pattern β€” lock core algorithm while allowing hooks
final fieldCannot be reassigned after initialisationprivate final String id;Immutable identity fields: IDs, keys, config values set in constructor
final paramCannot be reassigned inside the method bodyvoid method(final int x)Defensive coding β€” prevent accidental reassignment of parameters
β˜• JavaFinalInInheritance.java
// ── FINAL CLASS β€” cannot be extended ─────────────────
public final class SSLCertificate {
    private final String thumbprint;
    private final String issuer;

    public SSLCertificate(String thumbprint, String issuer) {
        this.thumbprint = thumbprint;
        this.issuer     = issuer;
    }
    // No subclass can tamper with certificate validation logic
}
// class FakeSSL extends SSLCertificate { } // ❌ Compile error

// ── FINAL METHOD β€” cannot be overridden ──────────────
public class OrderProcessor {

    // This core algorithm must never be changed by subclasses
    public final OrderResult process(Order order) {
        validate(order);          // Overridable hook
        applyDiscounts(order);    // Overridable hook
        PaymentResult p = chargePayment(order); // Overridable hook
        if (!p.isSuccess()) return OrderResult.failure(p.getReason());
        fulfil(order);            // Overridable hook
        sendConfirmation(order);  // Overridable hook
        return OrderResult.success(order);
    }

    // These CAN be overridden β€” customise the steps
    protected void validate(Order order)              { /* default impl */ }
    protected void applyDiscounts(Order order)        { /* default impl */ }
    protected PaymentResult chargePayment(Order order){ return PaymentResult.success(""); }
    protected void fulfil(Order order)                { /* default impl */ }
    protected void sendConfirmation(Order order)      { /* default impl */ }
}

// Subclass can customise steps but NOT the overall algorithm
public class ExpressOrderProcessor extends OrderProcessor {

    @Override
    protected void fulfil(Order order) {
        System.out.println("Priority fulfillment for order: " + order.getId());
        // Express-specific fulfilment logic
    }

    // @Override
    // public OrderResult process(Order o) { } // ❌ Compile error β€” process() is final
}

// ── FINAL FIELD β€” immutable after construction ────────
public class TransactionRecord {
    private final String transactionId;  // Set once β€” never changes
    private final long   timestamp;
    private       TransactionStatus status; // Mutable β€” status can change

    public TransactionRecord(String id) {
        this.transactionId = id;
        this.timestamp     = System.currentTimeMillis();
        this.status        = TransactionStatus.PENDING;
    }
    public String getTransactionId() { return transactionId; }
}

IS-A vs HAS-A β€” Choosing Inheritance or Composition

One of the most important design decisions in OOP is choosing between inheritance (IS-A) and composition (HAS-A). Misusing inheritance when composition is appropriate β€” or vice versa β€” leads to fragile, unmaintainable class hierarchies. The litmus test is always: does the relationship truly hold in every possible context?

🧬
IS-A β€” When to Use Inheritance

Use inheritance when: (1) The subclass genuinely IS a more specific type of the parent β€” a Dog IS-A Animal, a SavingsAccount IS-A BankAccount, an EmailNotification IS-A Notification. (2) The subclass can substitute for the parent in all situations (Liskov Substitution Principle). (3) You want to extend or specialise the parent's behaviour. Red flag: if you need to override a method and throw an exception to say 'not applicable', inheritance is wrong.

πŸ”—
HAS-A β€” When to Use Composition

Use composition when: (1) One class CONTAINS or USES another β€” a Car HAS-A Engine, an Order HAS-A PaymentMethod, a Logger HAS-A Formatter. (2) The relationship is 'uses' or 'is made of', not 'is a type of'. (3) You want flexibility β€” the 'has-a' component can be swapped at runtime. Composition is more flexible than inheritance β€” you can change behaviours by injecting different implementations.

βš–οΈ
Favour Composition Over Inheritance

The classic GoF design principle: 'Favour object composition over class inheritance'. Inheritance creates tight coupling β€” the subclass is intimately coupled to the parent's implementation. Adding a method to the parent can break subclasses. Composition is looser β€” the outer class only depends on the component's public API. In Spring and modern Java enterprise code, composition (dependency injection) is strongly preferred over deep inheritance trees.

β˜• JavaIsAHasADemo.java
// ── IS-A: Correct inheritance ─────────────────────────
public class Notification {
    protected String recipientId;
    protected String message;
    protected NotificationPriority priority;

    public Notification(String recipientId, String message,
                        NotificationPriority priority) {
        this.recipientId = recipientId;
        this.message     = message;
        this.priority    = priority;
    }

    public void send() {
        System.out.println("Sending to " + recipientId + ": " + message);
    }
}

// βœ… EmailNotification IS-A Notification β€” inheritance makes sense
public class EmailNotification extends Notification {
    private String subject;
    private String fromAddress;

    public EmailNotification(String recipientId, String subject,
                             String body, String from) {
        super(recipientId, body, NotificationPriority.NORMAL);
        this.subject     = subject;
        this.fromAddress = from;
    }

    @Override
    public void send() {
        System.out.printf("Email β†’ %s | Subject: %s | From: %s%n",
                          recipientId, subject, fromAddress);
    }
}

// ── HAS-A: Correct composition ────────────────────────
public class Engine {
    private int   horsePower;
    private String fuelType;

    public Engine(int hp, String fuelType) {
        this.horsePower = hp;
        this.fuelType   = fuelType;
    }
    public void start()  { System.out.println(horsePower + "hp engine started."); }
    public void stop()   { System.out.println("Engine stopped."); }
    public int  getHP()  { return horsePower; }
}

// βœ… Car HAS-A Engine β€” composition is correct
// ❌ Car extends Engine β€” WRONG: a Car is NOT a type of Engine
public class Car {
    private final Engine  engine;  // HAS-A relationship
    private       String  brand;
    private       boolean running;

    public Car(String brand, Engine engine) {
        this.brand  = brand;
        this.engine = engine; // Engine injected β€” can swap petrol/electric
        this.running = false;
    }

    public void start() {
        engine.start();  // DELEGATES to the Engine object
        running = true;
        System.out.println(brand + " is now running.");
    }

    // Benefit: engine can be swapped β€” ElectricEngine, HybridEngine, etc.
    public int getEnginePower() { return engine.getHP(); }
}

// Car car = new Car("Honda", new Engine(150, "Petrol"));
// car.start(); // 150hp engine started. β†’ Honda is now running.

instanceof Operator β€” Type Checking in Inheritance

The instanceof operator tests whether an object is an instance of a specific class, or of a class that inherits from it. It returns true if the object IS-A that type (directly or through inheritance), and false otherwise. Java 16+ enhances this with Pattern Matching instanceof β€” combining the type check with an automatic cast.

πŸ”
Classic instanceof

'obj instanceof ClassName' β€” returns true if obj is an instance of ClassName or any of its subclasses. Always returns false for null (never throws NullPointerException). Example: in an inheritance chain Animal β†’ Mammal β†’ Dog, a Dog object is instanceof Dog (true), instanceof Mammal (true), instanceof Animal (true), instanceof Object (true).

⚑
Pattern Matching instanceof (Java 16+)

'if (shape instanceof Circle c)' β€” tests AND casts in one step. If true, 'c' is automatically bound as a Circle reference β€” no explicit cast needed. The binding variable is in scope only where guaranteed to match. Eliminates the verbose test-then-cast boilerplate. Combined with && for additional conditions: 'if (obj instanceof String s && s.length() > 5)'.

🌳
instanceof in Inheritance Trees

A subclass object is instanceof its parent, grandparent, and all ancestors β€” up to Object. 'new Dog() instanceof Animal' β†’ true. 'new Dog() instanceof Object' β†’ true. This is the foundation of polymorphism β€” you can treat any object through its parent type interface. Use instanceof when you genuinely need to know the specific type; avoid it when polymorphism (method overriding) can handle it more cleanly.

β˜• JavaInstanceofDemo.java
public class InstanceofDemo {

    // ── Class hierarchy ───────────────────────────────
    static class Shape        { double getArea() { return 0; } }
    static class Circle    extends Shape {
        double radius;
        Circle(double r)    { radius = r; }
        @Override double getArea() { return Math.PI * radius * radius; }
    }
    static class Rectangle extends Shape {
        double w, h;
        Rectangle(double w, double h) { this.w=w; this.h=h; }
        @Override double getArea() { return w * h; }
    }
    static class Square extends Rectangle {
        Square(double side) { super(side, side); }
    }

    public static void main(String[] args) {

        Shape[] shapes = {
            new Circle(5),
            new Rectangle(4, 6),
            new Square(3),
            null
        };

        for (Shape s : shapes) {

            // ── Classic instanceof ────────────────────
            System.out.println("--- Next shape ---");
            System.out.println("instanceof Shape:     " + (s instanceof Shape));
            System.out.println("instanceof Circle:    " + (s instanceof Circle));
            System.out.println("instanceof Rectangle: " + (s instanceof Rectangle));
            System.out.println("instanceof Square:    " + (s instanceof Square));
            // null instanceof anything β†’ always false (no NPE!)
        }

        // ── Pattern Matching instanceof (Java 16+) ────
        Shape shape = new Circle(7.0);

        // Old way β€” verbose: test + separate cast
        if (shape instanceof Circle) {
            Circle c = (Circle) shape;   // Redundant cast
            System.out.println("Old way β€” Radius: " + c.radius);
        }

        // βœ… New way β€” test + bind in one step (Java 16+)
        if (shape instanceof Circle c) {
            System.out.println("New way β€” Radius: " + c.radius);
            System.out.println("Area: " + String.format("%.2f", c.getArea()));
        }

        // βœ… With extra condition on the bound variable
        if (shape instanceof Circle c && c.radius > 5.0) {
            System.out.println("Large circle β€” radius: " + c.radius);
        }

        // ── Preferred: Polymorphism over instanceof ───
        // Instead of instanceof + cast, rely on overriding:
        for (Shape s2 : new Shape[]{ new Circle(3), new Rectangle(2,4) }) {
            System.out.println("Area: " + String.format("%.2f", s2.getArea()));
            // No instanceof needed β€” dynamic dispatch handles it!
        }
    }
}

java.lang.Object β€” The Root of All Java Classes

Every class in Java implicitly extends java.lang.Object β€” even if you don't write extends Object. This means every object, no matter what class it belongs to, inherits a set of core methods from Object. Understanding these methods is essential β€” some require overriding to work correctly, and their contracts have precise rules.

MethodDefault BehaviourShould Override?When to Override
toString()ClassName@hexHashCode (rarely useful)βœ… Almost alwaysAny class whose instances will be printed or logged
equals(Object o)Reference equality (== same object identity)βœ… For value objectsWhen two different objects with same data should be 'equal'
hashCode()Based on memory address (JVM-specific)βœ… With equals()Whenever equals() is overridden β€” always as a pair
clone()Shallow copy (implements Cloneable interface)⚠️ RarelyWhen controlled copying is needed β€” prefer copy constructors
getClass()Returns Class<T> at runtime❌ Neverβ€”
finalize()Called before GC (deprecated since Java 9)❌ AvoidUse try-with-resources or Cleaner API instead
wait()/notify()Thread synchronisation primitives❌ NeverUse java.util.concurrent classes instead
β˜• JavaObjectMethodsDemo.java
import java.util.Objects;

public class OrderItem {

    private final String productId;
    private final String productName;
    private final int    quantity;
    private final double unitPrice;

    public OrderItem(String productId, String name, int qty, double price) {
        this.productId   = productId;
        this.productName = name;
        this.quantity    = qty;
        this.unitPrice   = price;
    }

    // ── toString: human-readable representation ───────
    @Override
    public String toString() {
        return String.format("OrderItem{id='%s', name='%s', qty=%d, price=β‚Ή%.2f, total=β‚Ή%.2f}",
               productId, productName, quantity, unitPrice, getTotal());
    }

    // ── equals: value-based equality ──────────────────
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                    // Same object
        if (!(o instanceof OrderItem item)) return false; // Different type or null
        return quantity == item.quantity
            && Double.compare(unitPrice, item.unitPrice) == 0
            && Objects.equals(productId, item.productId);
    }

    // ── hashCode: consistent with equals ──────────────
    @Override
    public int hashCode() {
        return Objects.hash(productId, quantity, unitPrice);
    }

    // Business method
    public double getTotal() { return quantity * unitPrice; }

    // Getters
    public String getProductId()   { return productId; }
    public String getProductName() { return productName; }
    public int    getQuantity()    { return quantity; }
    public double getUnitPrice()   { return unitPrice; }
}

// Usage demonstration:
// OrderItem a = new OrderItem("P001", "Laptop", 1, 65000);
// OrderItem b = new OrderItem("P001", "Laptop", 1, 65000);
// System.out.println(a);          // OrderItem{id='P001', name='Laptop'...}
// System.out.println(a.equals(b)); // true  β€” value equality
// System.out.println(a == b);      // false β€” different objects in memory
//
// var set = new java.util.HashSet<OrderItem>();
// set.add(a);
// set.contains(b); // true β€” works correctly with hashCode override

Liskov Substitution Principle β€” The Test of Good Inheritance

The Liskov Substitution Principle (LSP) β€” the 'L' in SOLID β€” states: if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program. In plain English: wherever you use a Parent object, you should be able to substitute a Child object and everything should still work correctly.

βœ…
LSP-Compliant Inheritance

A subclass honours the parent's contract: (1) Does not weaken preconditions β€” accepts at least the same inputs. (2) Does not strengthen postconditions β€” delivers at least the same guarantees. (3) Does not throw new exceptions not declared by the parent. (4) Preserves invariants β€” properties that always hold for the parent type. If Dog extends Animal and Animal.makeSound() is supposed to make a non-silent sound, Dog.makeSound() must also make a sound.

❌
LSP Violations β€” Classic Example

The Rectangle-Square problem: Square extends Rectangle seems logical mathematically. But if Rectangle has setWidth() and setHeight() and code sets them independently, Square breaks the contract β€” setting width also changes height (to stay square), violating the caller's expectation that width and height are independent. The fix: Square should NOT extend Rectangle β€” they should share a common Shape interface instead.

πŸ›‘οΈ
How to Detect LSP Violations

Warning signs: (1) Overriding a method to throw UnsupportedOperationException ('not applicable'). (2) Overriding a method to do nothing (empty body) when the parent's version does something important. (3) Strengthening input validation in an override (rejecting valid parent inputs). (4) Overriding to return null when the parent guarantees non-null. Any of these means inheritance is the wrong tool β€” use composition or a shared interface instead.

β˜• JavaLSPDemo.java
// ── LSP VIOLATION: Rectangle-Square Problem ───────────
class Rectangle {
    protected double width;
    protected double height;

    public void setWidth(double w)  { this.width  = w; }
    public void setHeight(double h) { this.height = h; }
    public double getArea()         { return width * height; }
}

// ❌ VIOLATES LSP β€” Square breaks Rectangle's behaviour contract
class Square extends Rectangle {
    @Override
    public void setWidth(double w) {
        this.width  = w;
        this.height = w; // Forces equal sides β€” breaks Rectangle contract!
    }
    @Override
    public void setHeight(double h) {
        this.width  = h; // Same problem
        this.height = h;
    }
}

// Code that works for Rectangle but BREAKS for Square:
static void testRectangle(Rectangle r) {
    r.setWidth(5);
    r.setHeight(3);
    // Expects area = 15 for ANY Rectangle
    assert r.getArea() == 15 : "Expected 15 but got " + r.getArea();
}
// testRectangle(new Rectangle()); // βœ… Passes
// testRectangle(new Square());    // ❌ Fails β€” area is 9, not 15!

// ── LSP-COMPLIANT FIX: Use abstraction instead ────────
interface Shape {
    double getArea();
    double getPerimeter();
}

class RectangleFixed implements Shape {
    private final double width;
    private final double height;
    RectangleFixed(double w, double h) { width=w; height=h; }
    @Override public double getArea()      { return width * height; }
    @Override public double getPerimeter() { return 2*(width+height); }
}

class SquareFixed implements Shape {
    private final double side;
    SquareFixed(double s) { side = s; }
    @Override public double getArea()      { return side * side; }
    @Override public double getPerimeter() { return 4 * side; }
}
// Both implement Shape β€” no LSP violation.
// Square is not forced to pretend it's a mutable Rectangle.

// ── LSP-COMPLIANT Inheritance Example ─────────────────
abstract class Discount {
    // Contract: always returns a value between 0.0 and 1.0
    public abstract double getDiscountRate(double orderValue);
}

class FestivalDiscount extends Discount {
    @Override
    public double getDiscountRate(double orderValue) {
        return orderValue > 5000 ? 0.20 : 0.10; // βœ… Always 0.0–1.0
    }
}

class PremiumMemberDiscount extends Discount {
    @Override
    public double getDiscountRate(double orderValue) {
        return 0.15; // βœ… Always 0.0–1.0 β€” LSP honoured
    }
}

Common Mistakes & Pitfalls β€” Bugs That Trip Everyone Up

These inheritance-related mistakes appear consistently in Java beginner and intermediate code. Each one either produces a compile-time error or β€” more dangerously β€” a silent runtime bug that is difficult to trace.

β˜• JavaInheritanceMistakes.java
// ❌ MISTAKE 1: Forgetting super() when parent has no no-arg constructor
class Vehicle {
    String brand;
    Vehicle(String brand) { this.brand = brand; } // No default constructor!
}
class Car extends Vehicle {
    int doors;
    Car(int doors) {
        // Java auto-inserts super() β€” but Vehicle has no no-arg constructor!
        // ❌ Compile error: no suitable constructor found
        this.doors = doors;
    }
}
// βœ… Fix: explicitly call super(args)
class CarFixed extends Vehicle {
    int doors;
    CarFixed(String brand, int doors) {
        super(brand);  // βœ… Calls Vehicle(String brand)
        this.doors = doors;
    }
}

// ❌ MISTAKE 2: @Override typo silently creates a NEW method
class Animal {
    public void makeSound() { System.out.println("..."); }
}
class Cat extends Animal {
    // Missing @Override β€” 'makesound' is a NEW method, not an override
    public void makesound() { System.out.println("Meow"); } // Silent bug!
}
// Animal a = new Cat();
// a.makeSound(); // Prints "..." β€” Cat's method never called!
// βœ… Fix: always add @Override β€” compiler will catch the typo

// ❌ MISTAKE 3: Narrowing access modifier in override
class Base {
    public void display() { System.out.println("Base"); }
}
class Derived extends Base {
    // @Override
    // protected void display() { } // ❌ Compile error β€” narrows public to protected
    @Override
    public void display() { System.out.println("Derived"); } // βœ… Same or wider
}

// ❌ MISTAKE 4: Calling overridable method from constructor
class Parent {
    public Parent() {
        printInfo(); // ❌ Dangerous β€” calls overridden version before subclass is ready
    }
    public void printInfo() { System.out.println("Parent info"); }
}
class Child extends Parent {
    private String info = "Child data";
    @Override
    public void printInfo() {
        System.out.println(info.toUpperCase()); // ❌ NPE! 'info' is null here
    }
}
// new Child() β†’ NullPointerException in printInfo() before info is set

// ❌ MISTAKE 5: instanceof misused β€” checking specific type when polymorphism works
// BAD: every time Animal adds a new subclass, this switch breaks
for (Animal a : animals) {
    if (a instanceof Dog)  { ((Dog)a).fetch(); }
    else if (a instanceof Cat)  { ((Cat)a).purr(); }
    else if (a instanceof Bird) { ((Bird)a).tweet(); }
}
// βœ… Better: use polymorphism β€” each subclass overrides 'performAction()'
for (Animal a : animals) {
    a.performAction(); // Dog.performAction() fetches, Cat.purr()s, etc.
}

Bad Practices & Anti-Patterns β€” What Senior Developers Reject

These inheritance anti-patterns are the most common causes of fragile, unmaintainable class hierarchies in professional Java codebases. Each one has a cleaner alternative.

🚫
Deep Inheritance Chains (The Gorilla-Banana Problem)

Classes 5+ levels deep β€” every change to the top of the hierarchy ripples to all descendants. Joe Armstrong (Erlang creator) famously said: 'You wanted a banana but you got a gorilla holding the banana and the entire jungle.' Deep chains are fragile, hard to understand, and signal that composition may be more appropriate. Rule: inheritance hierarchies beyond 3 levels are a code smell.

🚫
Inheriting for Code Reuse Alone

Using inheritance ONLY because a parent has methods you want β€” not because a true IS-A relationship exists. Example: 'class Stack extends ArrayList' to reuse ArrayList's storage. But a Stack IS NOT an ArrayList β€” Stack should restrict access to top only. Result: all ArrayList methods (get(), add(index), remove(index)) are exposed on Stack, breaking encapsulation. Fix: composition β€” Stack CONTAINS an ArrayList internally.

🚫
Overriding to Disable (UnsupportedOperationException)

Overriding a method and throwing UnsupportedOperationException to say 'this method doesn't apply here' is a clear LSP violation. If a subclass cannot honour the parent's method contract, the inheritance is wrong. Real example: java.util.Collections.unmodifiableList() adds() throws UnsupportedOperationException β€” it shouldn't extend a mutable list at all. Fix: use a separate interface or composition.

🚫
Exposing Parent Internals Through Protected Fields

Making fields 'protected' instead of 'private' because subclasses need them directly is lazy encapsulation. Protected fields tightly couple every subclass to the parent's internal representation. If the parent changes how it stores data, all subclasses break. Fix: keep fields private in the parent and provide protected methods (getters, callbacks) for subclasses to interact with them safely.

🚫
Concrete Class Inheritance Across Libraries

Extending a concrete class from a third-party library you don't control is risky β€” the library may add final methods, change behaviour, or the class may not be designed for extension. This creates a tight coupling to an external implementation detail. Fix: wrap the class with composition (Decorator/Adapter pattern). Only extend when the class was explicitly designed for extension.

🚫
Not Overriding equals/hashCode in Value Subclasses

If a subclass adds fields that are part of equality (e.g., a GoldMember extends Member, adds a 'membershipLevel' field), failing to override equals() and hashCode() in the subclass means two GoldMembers with different levels may compare as equal using the parent's equals(). This breaks Set and Map behaviour. JEP note: Java's Effective Java (Bloch) recommends avoiding extending concrete classes when value equality is involved β€” use composition.

Real-World Production Code Examples β€” Inheritance in Context

The following examples demonstrate carefully designed inheritance hierarchies from real enterprise Java applications β€” showing how inheritance, abstract classes, method overriding, and the template method pattern work together in production-grade code.

β˜• JavaReportGenerator.java β€” Template Method Pattern
package com.techsustainify.report;

import java.time.LocalDateTime;
import java.util.List;

/**
 * Abstract base class β€” defines the TEMPLATE for report generation.
 * The algorithm skeleton is fixed (final); steps are customisable (overridable).
 * Pattern: Template Method β€” GoF Behavioural Pattern
 */
public abstract class ReportGenerator {

    protected final String  reportTitle;
    protected final String  generatedBy;
    protected final LocalDateTime generatedAt;

    protected ReportGenerator(String title, String generatedBy) {
        this.reportTitle  = title;
        this.generatedBy  = generatedBy;
        this.generatedAt  = LocalDateTime.now();
    }

    // ── TEMPLATE METHOD β€” algorithm skeleton, cannot be overridden ──
    public final ReportOutput generate(ReportRequest request) {
        validateRequest(request);       // Hook 1
        List<?> data    = fetchData(request);   // Hook 2 β€” must override
        List<?> filtered = filterData(data, request); // Hook 3
        List<?> sorted   = sortData(filtered);         // Hook 4
        ReportBody body  = formatBody(sorted);         // Hook 5 β€” must override
        ReportHeader header = buildHeader(request);    // Hook 6
        ReportFooter footer = buildFooter();           // Hook 7
        return assembleReport(header, body, footer);
    }

    // ── ABSTRACT HOOKS β€” subclasses MUST implement ────
    protected abstract List<?> fetchData(ReportRequest request);
    protected abstract ReportBody formatBody(List<?> data);

    // ── OVERRIDABLE HOOKS β€” subclasses MAY customise ──
    protected void validateRequest(ReportRequest request) {
        if (request == null)
            throw new IllegalArgumentException("Request cannot be null");
    }

    protected List<?> filterData(List<?> data, ReportRequest request) {
        return data; // Default: no filtering
    }

    protected List<?> sortData(List<?> data) {
        return data; // Default: preserve order
    }

    protected ReportHeader buildHeader(ReportRequest request) {
        return new ReportHeader(reportTitle, generatedBy, generatedAt);
    }

    protected ReportFooter buildFooter() {
        return new ReportFooter("Page 1 of 1", "Confidential");
    }

    private ReportOutput assembleReport(ReportHeader h,
                                        ReportBody b, ReportFooter f) {
        return new ReportOutput(h, b, f);
    }
}

// ── CONCRETE SUBCLASS 1: Sales Report ─────────────────
public class SalesReportGenerator extends ReportGenerator {

    private final SalesRepository salesRepo;

    public SalesReportGenerator(SalesRepository repo, String generatedBy) {
        super("Monthly Sales Report", generatedBy);
        this.salesRepo = repo;
    }

    @Override
    protected List<SaleRecord> fetchData(ReportRequest request) {
        return salesRepo.findByDateRange(
               request.getFromDate(), request.getToDate());
    }

    @Override
    protected List<?> sortData(List<?> data) {
        // Sales reports sorted by date descending
        return ((List<SaleRecord>) data).stream()
               .sorted((a, b) -> b.getSaleDate().compareTo(a.getSaleDate()))
               .toList();
    }

    @Override
    protected ReportBody formatBody(List<?> data) {
        List<SaleRecord> sales = (List<SaleRecord>) data;
        double totalRevenue = sales.stream().mapToDouble(SaleRecord::getAmount).sum();
        return new SalesReportBody(sales, totalRevenue);
    }
}

// ── CONCRETE SUBCLASS 2: Inventory Report ─────────────
public class InventoryReportGenerator extends ReportGenerator {

    private final InventoryRepository inventoryRepo;
    private final int lowStockThreshold;

    public InventoryReportGenerator(InventoryRepository repo,
                                    int threshold, String generatedBy) {
        super("Inventory Status Report", generatedBy);
        this.inventoryRepo    = repo;
        this.lowStockThreshold = threshold;
    }

    @Override
    protected List<InventoryItem> fetchData(ReportRequest request) {
        return inventoryRepo.findAll();
    }

    @Override
    protected List<?> filterData(List<?> data, ReportRequest request) {
        // Only include low-stock items
        return ((List<InventoryItem>) data).stream()
               .filter(item -> item.getStock() < lowStockThreshold)
               .toList();
    }

    @Override
    protected ReportBody formatBody(List<?> data) {
        return new InventoryReportBody((List<InventoryItem>) data, lowStockThreshold);
    }
}

// Both generators use the SAME template (generate()) but produce
// completely different reports through method overriding.
β˜• JavaNotificationService.java β€” Hierarchical Inheritance
package com.techsustainify.notification;

import java.time.LocalDateTime;

// ── ROOT: Abstract Notification ───────────────────────
public abstract class BaseNotification {

    private final String id;
    private final String recipientId;
    private final String body;
    private final NotificationPriority priority;
    private final LocalDateTime        createdAt;
    private       NotificationStatus   status;

    protected BaseNotification(String recipientId, String body,
                               NotificationPriority priority) {
        this.id          = java.util.UUID.randomUUID().toString();
        this.recipientId = recipientId;
        this.body        = body;
        this.priority    = priority;
        this.createdAt   = LocalDateTime.now();
        this.status      = NotificationStatus.PENDING;
    }

    // Template method β€” fixed delivery flow
    public final DeliveryResult deliver() {
        try {
            if (!isRecipientReachable()) {
                status = NotificationStatus.FAILED;
                return DeliveryResult.failure("Recipient unreachable");
            }
            DeliveryResult result = doDeliver();
            status = result.isSuccess()
                   ? NotificationStatus.DELIVERED
                   : NotificationStatus.FAILED;
            return result;
        } catch (Exception e) {
            status = NotificationStatus.FAILED;
            return DeliveryResult.failure("Delivery error: " + e.getMessage());
        }
    }

    // Each notification type implements its own delivery
    protected abstract DeliveryResult doDeliver();
    protected abstract boolean isRecipientReachable();

    // Getters
    public String                getId()          { return id; }
    public String                getRecipientId() { return recipientId; }
    public String                getBody()        { return body; }
    public NotificationPriority  getPriority()    { return priority; }
    public NotificationStatus    getStatus()      { return status; }
    public boolean               isPending()      { return status == NotificationStatus.PENDING; }
}

// ── EMAIL NOTIFICATION ────────────────────────────────
public class EmailNotification extends BaseNotification {

    private final String subject;
    private final String toEmail;
    private final EmailClient emailClient;

    public EmailNotification(String recipientId, String toEmail,
                             String subject, String body,
                             EmailClient client) {
        super(recipientId, body, NotificationPriority.NORMAL);
        this.subject     = subject;
        this.toEmail     = toEmail;
        this.emailClient = client;
    }

    @Override
    protected boolean isRecipientReachable() {
        return toEmail != null && toEmail.contains("@");
    }

    @Override
    protected DeliveryResult doDeliver() {
        return emailClient.send(toEmail, subject, getBody());
    }
}

// ── SMS NOTIFICATION ──────────────────────────────────
public class SmsNotification extends BaseNotification {

    private final String phoneNumber;
    private final SmsGateway smsGateway;

    public SmsNotification(String recipientId, String phoneNumber,
                           String body, SmsGateway gateway) {
        super(recipientId, body, NotificationPriority.HIGH);
        this.phoneNumber = phoneNumber;
        this.smsGateway  = gateway;
    }

    @Override
    protected boolean isRecipientReachable() {
        return phoneNumber != null && phoneNumber.matches("[6-9]\\d{9}");
    }

    @Override
    protected DeliveryResult doDeliver() {
        return smsGateway.sendSms(phoneNumber, getBody());
    }
}

// ── Dispatch service uses parent type β€” polymorphism ──
public class NotificationDispatcher {
    public void dispatch(List<BaseNotification> notifications) {
        notifications.stream()
            .filter(BaseNotification::isPending)
            .forEach(n -> {
                DeliveryResult r = n.deliver(); // Dynamic dispatch
                System.out.printf("[%s] %s β†’ %s%n",
                    n.getClass().getSimpleName(),
                    n.getRecipientId(),
                    r.isSuccess() ? "Delivered" : "Failed: " + r.getReason());
            });
    }
}

Inheritance Hierarchy Flowchart β€” Object Creation Chain

This flowchart illustrates what happens at the JVM level when you create a subclass object β€” showing the constructor chain, method resolution, and memory layout.

β–Ά new SubClass() callede.g. new SavingsAccount(args)
allocate
πŸ“¦ JVM allocates memory on Heapspace for ALL fields: Object + Parent + Child
enter
πŸ”— SubClass() constructor beginsfirst statement auto-calls super()
super()
⬆️ Parent constructor runsinitialises parent fields
super()
⬆️ Object() constructor runsjava.lang.Object β€” root of all
return
πŸ”½ Return to Parent constructorcontinues after super() call
return
πŸ”½ Return to SubClass constructorsubclass fields initialised
build vtable
πŸ“‹ vtable built for objectmaps method names to implementations
done
βœ… Object reference returnedready for use β€” all fields initialised

Code Execution Flow β€” from source to output

Java Inheritance Interview Questions β€” Beginner to Advanced

These questions are consistently asked in Java fresher and experienced developer interviews, campus placements, and OCPJP certification exams.

Practice Questions β€” Test Your Inheritance Knowledge

Attempt each question independently before reading the answer β€” active recall significantly improves retention and understanding.

1. What is the output? class A { A() { System.out.println("A"); } } class B extends A { B() { System.out.println("B"); } } class C extends B { C() { System.out.println("C"); } } public class Test { public static void main(String[] args) { new C(); } }

Easy

2. Will this compile? If not, why? class Animal { Animal(String name) { System.out.println("Animal: " + name); } } class Dog extends Animal { Dog() { System.out.println("Dog created"); } }

Easy

3. What is the output? class Parent { String name = "Parent"; void display() { System.out.println("Parent: " + name); } } class Child extends Parent { String name = "Child"; // Field hiding @Override void display() { System.out.println("Child: " + name); } } public class Test { public static void main(String[] args) { Parent p = new Child(); p.display(); System.out.println(p.name); } }

Medium

4. Design a class hierarchy for a Shape system: Shape (abstract, getArea(), getPerimeter()), Circle, Rectangle, and Square. Square should use composition or correct inheritance. Add a ShapeCalculator that works with any Shape.

Medium

5. Identify the LSP violation: class Bird { public void fly() { System.out.println("Flying..."); } public void eat() { System.out.println("Eating..."); } } class Penguin extends Bird { @Override public void fly() { throw new UnsupportedOperationException("Penguins cannot fly!"); } }

Medium

6. What is the output? class Base { static void staticMethod() { System.out.println("Base static"); } void instanceMethod() { System.out.println("Base instance"); } } class Derived extends Base { static void staticMethod() { System.out.println("Derived static"); } @Override void instanceMethod() { System.out.println("Derived instance"); } } public class Test { public static void main(String[] args) { Base obj = new Derived(); obj.staticMethod(); obj.instanceMethod(); } }

Hard

7. Write a multilevel inheritance example: LivingBeing β†’ Animal β†’ Pet β†’ Dog. Each class adds one field and one method. Show constructor chaining.

Hard

8. Explain why 'class Stack extends ArrayList' is a bad design. What is the correct approach?

Hard

Conclusion β€” Inheritance: Power with Responsibility

Inheritance is one of Java's most powerful features β€” and one of the most frequently misused. When used correctly, it enables clean code reuse, elegant polymorphism, and extensible architectures. When misused β€” for code reuse without a true IS-A relationship, or in violation of LSP β€” it creates fragile, tightly-coupled hierarchies that are painful to maintain.

The professional's approach: always ask the IS-A question first. Does this relationship hold in every context, without exception? Does the subclass honour the parent's contract completely? Can I substitute the child wherever the parent is expected? If yes to all β€” inheritance is right. If no β€” composition is likely the better tool. The Template Method, Decorator, Strategy, and Composite patterns often provide better design alternatives than deep inheritance trees.

ConceptKey RuleExample
extendsDeclares IS-A β€” subclass inherits non-private parent membersclass Car extends Vehicle
super(args)Call parent constructor β€” first statement in subclass constructorsuper(brand, model, year)
super.method()Call parent's overridden version β€” extend, not replacereturn super.getDetails() + extra
@OverrideAlways annotate overrides β€” catches typos and mistakes@Override public void startEngine()
Method OverridingSame name+params, resolved at RUNTIME via dynamic dispatchAnimal a = new Dog(); a.speak()
final classCannot be extended β€” protects complete implementationspublic final class String
final methodCannot be overridden β€” locks algorithm in Template Methodpublic final OrderResult process()
instanceofType check β€” true for the class and all its ancestorsnew Dog() instanceof Animal β†’ true
IS-A vs HAS-AInherit only true subtypes; compose for 'uses/contains'Car IS-A Vehicle; Car HAS-A Engine
LSPSubclass must work wherever parent works β€” no contract breakingSquare should NOT extend Rectangle

Your next step: Java Polymorphism β€” where you will see how inheritance and method overriding combine to create programs that work uniformly across many types, how interfaces extend polymorphism beyond class hierarchies, and how modern Java (sealed classes, records, pattern matching switch) takes these concepts further. β˜•

Frequently Asked Questions β€” Java Inheritance