Senior 10 min · March 05, 2026

Java Inheritance — Parent Method Change Broke 30 Classes

Parent method change broke 30 child classes with VerifyError in production.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Java inheritance creates an IS-A relationship via the extends keyword
  • Child classes inherit all non-private members from a single parent
  • Method overriding with @Override enables polymorphic behaviour
  • Dynamic dispatch selects the correct method at runtime based on actual object type
  • Production risk: fragile base class problems when changing parent breaks children
  • Always apply the IS-A test before choosing inheritance over composition
✦ Definition~90s read
What is Inheritance in Java?

Java inheritance is a core mechanism where a class (child/subclass) derives fields and methods from another class (parent/superclass) using the extends keyword. It exists to enable code reuse and establish an "is-a" relationship — a Dog is an Animal, so Dog inherits Animal's behavior without rewriting it.

Think of a smartphone.

Java enforces single inheritance for classes (a class can extend only one parent) to avoid the diamond problem, but supports multilevel (A→B→C) and hierarchical (one parent, many children). You reach for inheritance when you have a clear taxonomic hierarchy and shared behavior; you avoid it when composition (has-a) or interfaces (contracts) better model the relationship, as deep inheritance chains become brittle — changing a parent method can silently break dozens of subclasses, which is exactly the scenario this article addresses.

In practice, inheritance is a compile-time coupling: the child class gets a direct dependency on the parent's implementation. Method overriding lets a child replace inherited behavior with its own version, but the super keyword gives explicit access to the parent's version, and constructor chaining ensures the parent's initialization runs first (via an implicit super() call).

Java 16+ sealed classes (sealed, permits) restrict which classes can extend a parent, giving you control over the inheritance hierarchy and enabling exhaustive pattern matching with instanceof — a safer alternative to open-ended inheritance. The tradeoff is real: inheritance is powerful for frameworks (e.g., HttpServletMyServlet) but dangerous in large codebases where a single parent change cascades through 30+ subclasses, as the article's title warns.

Alternatives include interfaces (for pure contracts), composition (delegating behavior to contained objects), and abstract classes (for partial implementation). When you see extends in a codebase, ask: is this a genuine "is-a" relationship, or would composition reduce coupling?

The Java standard library itself uses inheritance sparingly in newer APIs — List.of() returns an unmodifiable list via composition, not inheritance. Understanding these tradeoffs is what separates senior engineers from those who wake up to 30 broken classes after a parent method change.

Plain-English First

Think of a smartphone. Every smartphone — whether it's a Samsung, an iPhone, or a Pixel — has things in common: a screen, a battery, and the ability to make calls. Instead of redesigning those parts from scratch for every brand, manufacturers start with a 'base phone' blueprint and then add their own unique features on top. In Java, inheritance works exactly like that. You write a base class once, and every other class that 'inherits' from it automatically gets all its features — no copy-paste required.

Every production Java codebase you'll ever work in uses inheritance. It's not an academic exercise — it's the mechanism that lets a PaymentProcessor class share core logic with CreditCardProcessor and PayPalProcessor without duplicating a single line. When a bug is fixed in the base class, every subclass benefits instantly. That's not just convenient, it's the kind of thing that separates maintainable software from a tangled mess of duplicated code.

The problem inheritance solves is code duplication across related types. Imagine you're building an e-commerce platform. You have products: Books, Electronics, and Clothing. Every product has a name, a price, and a method to display itself. Without inheritance, you'd write those fields and methods three times, then fix bugs three times, and explain the logic to three different future teammates. Inheritance lets you define shared behaviour exactly once in a parent class and let child classes focus only on what makes them different.

By the end of this article you'll understand not just how to write the extends keyword, but why Java was designed this way, when inheritance is the right tool versus when it isn't, how method overriding actually works under the hood, and the patterns senior engineers use in real projects. You'll also know the mistakes that trip up even experienced developers — and how to avoid them.

What Inheritance Actually Does — The extends Keyword Unpacked

When one class extends another, the child class inherits every non-private field and method from the parent. That means the child class can use them as if it had written them itself. The parent is often called the superclass or base class; the child is called the subclass or derived class.

Java uses single inheritance for classes — each class can only extend one parent. This is a deliberate design choice to avoid the 'Diamond Problem' (where two parents both define the same method and the child doesn't know which one to use). If you need behaviour from multiple sources, Java's answer is interfaces — but that's a separate topic.

Here's the critical insight most tutorials skip: inheritance models an IS-A relationship. A Dog IS-A Animal. A SavingsAccount IS-A BankAccount. If you can't make that sentence sound natural, you probably shouldn't be using inheritance — you should be using composition instead. Inheritance locks in a tight relationship between two classes, so getting the design right from the start saves you painful refactors later.

BankAccountDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// A realistic banking example showing why inheritance saves us from duplication

// PARENT CLASS — defines everything ALL bank accounts share
class BankAccount {
    private String accountHolder;  // private: only accessible within this class
    protected double balance;      // protected: accessible to subclasses

    public BankAccount(String accountHolder, double initialBalance) {
        this.accountHolder = accountHolder;
        this.balance = initialBalance;
    }

    // Every account type can deposit money — logic lives here ONCE
    public void deposit(double amount) {
        if (amount <= 0) {
            System.out.println("Deposit amount must be positive.");
            return;
        }
        balance += amount;
        System.out.println(accountHolder + " deposited $" + amount + ". New balance: $" + balance);
    }

    public double getBalance() {
        return balance;
    }

    public String getAccountHolder() {
        return accountHolder;
    }

    public void printSummary() {
        System.out.println("Account: " + accountHolder + " | Balance: $" + balance);
    }
}

// CHILD CLASS — inherits everything from BankAccount, adds its own rules
class SavingsAccount extends BankAccount {
    private double interestRate;  // unique to SavingsAccount

    public SavingsAccount(String accountHolder, double initialBalance, double interestRate) {
        // super() calls the PARENT constructor — must be the first line
        super(accountHolder, initialBalance);
        this.interestRate = interestRate;
    }

    // Behaviour unique to savings accounts — the parent has no concept of this
    public void applyMonthlyInterest() {
        double interest = balance * interestRate;
        balance += interest;  // 'balance' is accessible because it's protected in parent
        System.out.println("Interest of $" + String.format("%.2f", interest) + " applied. New balance: $" + String.format("%.2f", balance));
    }
}

// ANOTHER CHILD CLASS — same parent, different specialisation
class CheckingAccount extends BankAccount {
    private double overdraftLimit;

    public CheckingAccount(String accountHolder, double initialBalance, double overdraftLimit) {
        super(accountHolder, initialBalance);
        this.overdraftLimit = overdraftLimit;
    }

    // Checking accounts allow withdrawal up to the overdraft limit
    public void withdraw(double amount) {
        if (amount > balance + overdraftLimit) {
            System.out.println("Withdrawal denied: exceeds overdraft limit.");
        } else {
            balance -= amount;
            System.out.println(getAccountHolder() + " withdrew $" + amount + ". Balance: $" + String.format("%.2f", balance));
        }
    }
}

public class BankAccountDemo {
    public static void main(String[] args) {
        SavingsAccount savings = new SavingsAccount("Alice", 1000.00, 0.03);
        savings.deposit(500.00);         // inherited from BankAccount — no duplication
        savings.applyMonthlyInterest();  // unique to SavingsAccount
        savings.printSummary();          // inherited from BankAccount

        System.out.println("---");

        CheckingAccount checking = new CheckingAccount("Bob", 200.00, 150.00);
        checking.deposit(100.00);        // same deposit() method, reused via inheritance
        checking.withdraw(400.00);       // unique to CheckingAccount
        checking.printSummary();         // inherited from BankAccount
    }
}
Output
Alice deposited $500.0. New balance: $1500.0
Interest of $45.00 applied. New balance: $1545.00
Account: Alice | Balance: $1545.0
---
Bob deposited $100.0. New balance: $300.0
Bob withdrew $400.0. Balance: $-100.00
Account: Bob | Balance: $-100.0
Pro Tip: protected vs private in Parent Classes
Use protected for fields you intend subclasses to access directly (like balance above). Use private for fields that should only be touched through getter/setter methods, even by subclasses. Defaulting everything to protected is a common shortcut that leaks internal state — be deliberate about it.
Production Insight
A common production pain is when a parent class removes a protected field that child classes relied on.
Compile-time errors catch it if child classes are recompiled — but if only the parent JAR is updated, runtime NoSuchFieldError can bring down services.
Rule: never remove or change access modifiers of protected members without auditing all child classes across your entire deployment.
Use interface-based contracts to decouple child classes from parent implementation details.
Key Takeaway
Inheritance models an IS-A relationship; if it sounds wrong in plain English, use composition.
Java enforces single inheritance to avoid the diamond problem — use interfaces for multiple behavioural contracts.
Always ask: "Would I be okay refactoring this parent class later?" If not, inheritance is not the answer.
Java Inheritance Pitfalls — Parent Change Impact THECODEFORGE.IO Java Inheritance Pitfalls — Parent Change Impact How modifying a parent class can break many subclasses Parent Class Change Modifying a method in the superclass Subclass Override Child class overrides the changed method Constructor Chaining super() call may break if parent constructor changes Broken Subclasses 30 classes fail due to unexpected behavior Composition Alternative Use delegation instead of inheritance ⚠ Changing a parent method can silently break all subclasses Favor composition over inheritance for flexible design THECODEFORGE.IO
thecodeforge.io
Java Inheritance Pitfalls — Parent Change Impact
Inheritance Java

Types of Inheritance in Java — Single, Multilevel, Hierarchical

Java supports three main forms of inheritance through the extends keyword. Understanding each type is crucial for designing robust class hierarchies.

1. Single Inheritance — one child class extends one parent class. This is the most common form. Example: class Dog extends Animal.

2. Multilevel Inheritance — a chain of inheritance where a class extends another class that itself extends a third class. Example: class Puppy extends Dog extends Animal. While allowed, deep multilevel hierarchies are brittle in production.

3. Hierarchical Inheritance — multiple child classes extend the same parent class. Example: class SavingsAccount extends BankAccount and class CheckingAccount extends BankAccount. This models one-to-many specialisation.

Java does not support multiple inheritance (one class extending multiple classes) to avoid the diamond problem. The diagram below visualises these three types.

InheritanceTypesDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Single inheritance
class Animal {
    void breathe() { System.out.println("Breathing..."); }
}
class Dog extends Animal {
    void bark() { System.out.println("Woof!"); }
}

// Multilevel inheritance
class Mammal extends Animal {
    void feedMilk() { System.out.println("Feeding milk"); }
}
class Human extends Mammal {
    void speak() { System.out.println("Hello"); }
}

// Hierarchical inheritance
class Vehicle {
    void move() { System.out.println("Moving..."); }
}
class Car extends Vehicle {
    void honk() { System.out.println("Beep!"); }
}
class Bike extends Vehicle {
    void ringBell() { System.out.println("Ring!"); }
}
Java Only Allows Single Inheritance for Classes
A class can only extend one parent class. This prevents the diamond problem. Use interfaces for multiple inheritance of type.
Production Insight
Multilevel inheritance beyond three levels is a frequent source of production incidents. Changing a grandparent method can silently break behaviour in distant subclasses. In one incident, a team added a validation step in a base class constructor, which broke child classes that had never overridden anything — simply because they relied on the old initialization order. Keep hierarchies shallow (max 2–3 levels).
Key Takeaway
Use single inheritance by default. Multilevel hierarchies are risky and require careful testing. Hierarchical inheritance is safe and common. Always question whether a deep chain is necessary.
Types of Inheritance
AnimalDogMammalHumanVehicleCarBike

Advantages and Disadvantages of Inheritance

Inheritance is a powerful tool but comes with trade-offs. The table below summarises the key advantages and disadvantages you must consider before deciding to use inheritance in your design.

AdvantageDisadvantage
Code reusability — write once, use in all subclassesTight coupling — changes in parent can break children
Polymorphism — treat different objects uniformly via parent referenceFragile base class — parent changes require auditing all children
Logical hierarchy — models real-world IS-A relationshipsSingle inheritance limitation — can't inherit from multiple classes
Easy extension — add new subclasses without modifying existing codeDeep hierarchies become complex and hard to debug
Method overriding enables custom behaviorCan expose implementation details via protected members
Built into Java language — no extra libraries neededMay lead to less flexibility than composition for changing requirements

Each advantage must be weighed against the corresponding disadvantage in your specific context. The tighter the coupling, the more careful you must be when maintaining the hierarchy.

Balance is Key
Inheritance is not inherently bad. Use it when the IS-A relationship is clear and the hierarchy is shallow. For any other case, consider composition first.
Production Insight
The tight coupling disadvantage is the most common source of inheritance-related production incidents. In a project I audited, a base class change intended to fix a security vulnerability broke 12 child classes because they overrode methods and the new parent method added a pre-condition they didn't expect. The fix took three weeks of rework across teams. Always document the contract for overridable methods.
Key Takeaway
Weigh advantages against disadvantages before using inheritance. Use it when code reuse and polymorphic behaviour outweigh the cost of coupling. Prefer composition for flexibility.

Using instanceof Pattern Matching with Sealed Classes (Java 16+)

Java 16 introduced pattern matching for instanceof, allowing you to cast and bind a variable in one step. When combined with sealed classes, you get exhaustive pattern matching that makes type-safe dispatches concise and compiler-verified.

Sealed classes (introduced in Java 17 as a standard feature) let you restrict which classes can extend a given parent. This is a game-changer for controlling inheritance in production — no more unexpected subclasses sneaking in from other modules.

The following example models a shape hierarchy with pattern matching in a switch expression. The compiler enforces that all permitted subclasses are covered, eliminating the risk of missing a case.

io/thecodeforge/inheritance/SealedPatternMatchDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Sealed interface — only Circle, Rectangle, Triangle can implement it
sealed interface Shape permits Circle, Rectangle, Triangle {}

final class Circle implements Shape {
    double radius;
    Circle(double radius) { this.radius = radius; }
}

final class Rectangle implements Shape {
    double width, height;
    Rectangle(double width, double height) { this.width = width; this.height = height; }
}

final class Triangle implements Shape {
    double base, height;
    Triangle(double base, double height) { this.base = base; this.height = height; }
}

public class ShapeProcessor {
    public double calculateArea(Shape shape) {
        // Pattern matching switch — compiler warns if any permitted type is missing
        return switch (shape) {
            case Circle c -> Math.PI * c.radius * c.radius;
            case Rectangle r -> r.width * r.height;
            case Triangle t -> 0.5 * t.base * t.height;
        };
    }

    public static void main(String[] args) {
        ShapeProcessor sp = new ShapeProcessor();
        Shape circle = new Circle(5);
        System.out.println("Circle area: " + sp.calculateArea(circle));

        Shape rect = new Rectangle(4, 6);
        System.out.println("Rectangle area: " + sp.calculateArea(rect));
    }
}
Output
Circle area: 78.53981633974483
Rectangle area: 24.0
Pattern Matching Avoids Casting Boilerplate
Before Java 16, you'd write: if (shape instanceof Circle) { Circle c = (Circle) shape; ... }. Now you combine the check and cast into one line: case Circle c -> ...
Production Insight
Sealed classes prevent rogue subclasses from being created in other modules, a common source of 'instanceof' blow-ups in large codebases. Combined with pattern matching, you get exhaustive coverage enforced at compile time. In a microservice architecture, this is invaluable — you control exactly which subclasses exist and the compiler catches missing cases before deployment.
Key Takeaway
Use sealed classes to tightly control inheritance hierarchies. Pair with pattern matching for safe, concise, and exhaustive type dispatches.

Method Overriding — Giving Inherited Behaviour Your Own Spin

Inheritance lets you reuse a method, but overriding lets you replace it with a better version specific to the child class. This is where inheritance really earns its keep in real projects.

To override a method, the child class defines a method with the exact same name, return type, and parameter list as the parent. Java's @Override annotation isn't strictly required — but you should always use it. It tells the compiler 'I intend to override a parent method here.' If you make a typo in the method name and it doesn't match the parent, the compiler will catch it and throw an error. Without @Override, it silently creates a new method instead, which is a nasty bug to track down.

The super keyword is your escape hatch. Inside an overriding method, super.methodName() calls the parent's original version. This is incredibly useful when you want to extend the parent's behaviour rather than completely replace it — think logging, validation, or adding a pre-step before the parent's core logic runs.

Under the hood, this works through dynamic dispatch: when you call a method on an object, Java looks at the actual runtime type of the object — not the declared type — to decide which version to run. This is the foundation of polymorphism.

ShippingCalculatorDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// Real-world example: a shipping system where each carrier calculates cost differently

class ShippingCarrier {
    protected String carrierName;
    protected double baseRatePerKg;

    public ShippingCarrier(String carrierName, double baseRatePerKg) {
        this.carrierName = carrierName;
        this.baseRatePerKg = baseRatePerKg;
    }

    // Default shipping cost — child classes will override this
    public double calculateShippingCost(double weightKg, double distanceKm) {
        return baseRatePerKg * weightKg;
    }

    public void printQuote(double weightKg, double distanceKm) {
        // Calls calculateShippingCost() — at runtime, Java runs the CHILD's version
        // if this object is actually a child instance (dynamic dispatch in action)
        double cost = calculateShippingCost(weightKg, distanceKm);
        System.out.println(carrierName + " quote: $" + String.format("%.2f", cost));
    }
}

class ExpressCarrier extends ShippingCarrier {
    private double expressMultiplier;

    public ExpressCarrier(String carrierName, double baseRatePerKg, double expressMultiplier) {
        super(carrierName, baseRatePerKg);
        this.expressMultiplier = expressMultiplier;
    }

    @Override  // Always use this — protects you from typo bugs
    public double calculateShippingCost(double weightKg, double distanceKm) {
        // Call the parent's calculation first, then add the express premium
        double baseCost = super.calculateShippingCost(weightKg, distanceKm);
        return baseCost * expressMultiplier;
    }
}

class FreightCarrier extends ShippingCarrier {
    private double costPerKm;

    public FreightCarrier(String carrierName, double baseRatePerKg, double costPerKm) {
        super(carrierName, baseRatePerKg);
        this.costPerKm = costPerKm;
    }

    @Override
    public double calculateShippingCost(double weightKg, double distanceKm) {
        // Freight pricing is completely different — weight AND distance matter
        double weightCost = baseRatePerKg * weightKg;
        double distanceCost = costPerKm * distanceKm;
        return weightCost + distanceCost;
    }
}

public class ShippingCalculatorDemo {
    public static void main(String[] args) {
        double packageWeight = 5.0;   // kg
        double shippingDistance = 200; // km

        // Declared as ShippingCarrier, but each object is a different subtype
        // printQuote() calls the CORRECT calculateShippingCost() for each — that's polymorphism
        ShippingCarrier standardCarrier = new ShippingCarrier("PostCo Standard", 2.50);
        ShippingCarrier expressCarrier  = new ExpressCarrier("QuickShip Express", 2.50, 2.0);
        ShippingCarrier freightCarrier  = new FreightCarrier("HeavyHaul Freight", 1.80, 0.05);

        standardCarrier.printQuote(packageWeight, shippingDistance);
        expressCarrier.printQuote(packageWeight, shippingDistance);  // runs ExpressCarrier's override
        freightCarrier.printQuote(packageWeight, shippingDistance);  // runs FreightCarrier's override
    }
}
Output
PostCo Standard quote: $12.50
QuickShip Express quote: $25.00
HeavyHaul Freight quote: $19.00
Watch Out: Forgetting @Override Costs You Hours
If you write public double calculateshippingCost(...) (lowercase 's') without @Override, Java won't error — it'll quietly create a second, separate method. Your parent's printQuote() will keep calling the original version and you'll spend an afternoon confused about why your override has no effect. Always use @Override. Always.
Production Insight
I've seen a production outage caused by a developer renaming a method in the parent class but forgetting to update a single child class that had @Override — the compiler caught it. Another team had no @Override annotations and three child classes silently hid the method with different signatures.
Result: the core business logic used a different implementation for those three child classes, causing incorrect pricing for 10% of orders.
Rule: add a CI checkstyle rule that requires @Override on all overriding methods — it's the cheapest insurance you'll get.
Key Takeaway
Use @Override on every overriding method — it's not optional, it's your safety net.
super.methodName() lets you extend parent behaviour, not replace it entirely.
Dynamic dispatch ensures the correct overridden method runs based on the actual object type, not the reference type.

The super Keyword and Constructor Chaining — What Really Happens at Object Creation

When you create a child class object, Java doesn't just run the child's constructor. It runs the parent's constructor first. Every time. This guarantees the parent's portion of the object is fully set up before the child tries to build on top of it.

If you explicitly call super(...) with arguments, it forwards those arguments to the matching parent constructor. If you don't call super() at all, Java silently inserts a call to the parent's no-argument constructor. If that no-argument constructor doesn't exist in the parent, you get a compile error — which confuses a lot of people the first time they see it.

super also lets you call parent methods (not just constructors) from inside the child. This is the key to the 'extend, don't replace' pattern: call super.someMethod() to run the parent's logic, then add your own lines after it. This pattern is everywhere in Android development, Spring Framework lifecycle methods, and GUI toolkits.

One hard rule you must know: super() must be the first statement in a constructor. You can't run any other code, set any fields, or do any checks before calling it. This is enforced by the compiler, not just a convention.

EmployeeHierarchyDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// Demonstrates constructor chaining and super method calls in a real HR system

class Employee {
    private String fullName;
    private String employeeId;
    private double annualSalary;

    public Employee(String fullName, String employeeId, double annualSalary) {
        this.fullName = fullName;
        this.employeeId = employeeId;
        this.annualSalary = annualSalary;
        System.out.println("[Employee constructor] Creating employee: " + fullName);
    }

    public double calculateAnnualBonus() {
        // Base employees get 5% of salary as bonus
        return annualSalary * 0.05;
    }

    public void printCompensationReport() {
        System.out.println("--- Compensation Report ---");
        System.out.println("Name:       " + fullName);
        System.out.println("ID:         " + employeeId);
        System.out.println("Salary:     $" + String.format("%.2f", annualSalary));
        System.out.println("Bonus:      $" + String.format("%.2f", calculateAnnualBonus()));
    }

    public double getAnnualSalary() {
        return annualSalary;
    }
}

class SeniorEngineer extends Employee {
    private int yearsOfExperience;
    private String specialisation;

    public SeniorEngineer(String fullName, String employeeId, double annualSalary,
                          int yearsOfExperience, String specialisation) {
        // super() MUST be first — sets up the Employee part of this object
        super(fullName, employeeId, annualSalary);
        this.yearsOfExperience = yearsOfExperience;
        this.specialisation = specialisation;
        System.out.println("[SeniorEngineer constructor] Adding specialisation: " + specialisation);
    }

    @Override
    public double calculateAnnualBonus() {
        // Start with what the parent would give, then add extra for experience
        double baseBonus = super.calculateAnnualBonus();  // calls Employee's 5% logic
        double experienceBonus = yearsOfExperience * 500.0; // $500 per year of experience
        return baseBonus + experienceBonus;
    }

    @Override
    public void printCompensationReport() {
        super.printCompensationReport();  // Print everything the parent prints first
        // Then append child-specific information
        System.out.println("Specialisation: " + specialisation);
        System.out.println("Experience: " + yearsOfExperience + " years");
    }
}

public class EmployeeHierarchyDemo {
    public static void main(String[] args) {
        System.out.println("=== Creating a SeniorEngineer ===");
        // Watch the order of constructor calls in the output
        SeniorEngineer lead = new SeniorEngineer(
            "Maya Chen", "ENG-0042", 120000.00, 8, "Distributed Systems"
        );

        System.out.println();
        lead.printCompensationReport();
    }
}
Output
=== Creating a SeniorEngineer ===
[Employee constructor] Creating employee: Maya Chen
[SeniorEngineer constructor] Adding specialisation: Distributed Systems
--- Compensation Report ---
Name: Maya Chen
ID: ENG-0042
Salary: $120000.00
Bonus: $10000.00
Specialisation: Distributed Systems
Experience: 8 years
Interview Gold: Why Does the Parent Constructor Run First?
Java guarantees the parent portion of an object is fully initialised before the child can use it. If the child constructor ran first and tried to call an inherited method before the parent was set up, you'd get partially-constructed objects and unpredictable behaviour. This ordering is why super() must be the first statement — the compiler enforces what good design demands.
Production Insight
A colleague once added logging to a child constructor before the super() call, causing a compile error they couldn't figure out — they had placed a system.out.println before super(). The fix was simple but wasted 30 minutes.
In production, constructor chains matter most when you have deep hierarchies (3+ levels) with complex initialisation. A change in a grandparent constructor can silently break all descendants.
Rule: keep parent constructors simple and avoid calling overridable methods from constructors — the child object isn't fully initialised yet.
Key Takeaway
Parent constructor always runs before child constructor — super() must be the first statement.
If the parent has no no-arg constructor, you must explicitly call super(args) or you get a compile error.
Use the 'extend then add' pattern with super.method() to enhance parent behaviour without replacing it entirely.

When NOT to Use Inheritance — Composition vs Inheritance in the Real World

Inheritance is powerful, but it's one of the most overused patterns in Java. Senior engineers know that composition is often the better choice — and knowing the difference is what separates intermediate from advanced thinking.

The rule of thumb is the IS-A vs HAS-A test. A Car IS-A Vehicle — inheritance makes sense. A Car HAS-A Engine — that's composition; you don't extend Engine, you hold a reference to one. When you get this wrong, you end up with brittle class hierarchies where changing a parent class breaks child classes in unexpected ways (this is called the 'fragile base class' problem).

Another red flag is when you're extending a class just to reuse its methods, not because the child truly is a kind of that parent. Stack in the Java standard library infamously extends Vector — a Stack IS-NOT-A Vector, but Java's designers made that call for code reuse. The result? You can call get(index) on a Stack, which makes no semantic sense for a stack data structure. That's what bad inheritance looks like in production.

Use inheritance when you have a genuine IS-A relationship AND you want polymorphic behaviour (treating different subtypes through a shared parent type). Otherwise, default to composition.

CompositionVsInheritanceDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// Demonstrates the right and wrong way to model a 'NotificationService'

// ============================================================
// WRONG APPROACH: Extending EmailSender just to reuse its methods
// A NotificationService IS-NOT-A EmailSender — it just USES one
// ============================================================
class EmailSender {
    public void sendEmail(String recipient, String subject, String body) {
        System.out.println("Sending email to " + recipient + " | Subject: " + subject);
    }
}

// BAD: This locks NotificationService into always being an EmailSender
// You can never easily swap in a different sender, and the public API
// of NotificationService now leaks all of EmailSender's methods
class NotificationServiceBad extends EmailSender {
    public void notifyUser(String userEmail, String message) {
        // Calling the inherited method — but this is NOT an IS-A relationship
        sendEmail(userEmail, "Notification", message);
    }
}

// ============================================================
// RIGHT APPROACH: Composition — NotificationService HAS-A sender
// ============================================================
interface MessageSender {
    // An interface means we can swap the implementation at any time
    void send(String recipient, String message);
}

class EmailMessageSender implements MessageSender {
    @Override
    public void send(String recipient, String message) {
        System.out.println("[Email] To: " + recipient + " | " + message);
    }
}

class SmsMessageSender implements MessageSender {
    @Override
    public void send(String recipient, String message) {
        System.out.println("[SMS] To: " + recipient + " | " + message);
    }
}

// GOOD: NotificationService holds a reference to any MessageSender
// Swapping from Email to SMS requires zero changes to this class
class NotificationServiceGood {
    private MessageSender messageSender;  // HAS-A relationship

    // Inject the sender — this is the Dependency Injection pattern
    public NotificationServiceGood(MessageSender messageSender) {
        this.messageSender = messageSender;
    }

    public void notifyUser(String recipient, String alertMessage) {
        messageSender.send(recipient, alertMessage);
    }
}

public class CompositionVsInheritanceDemo {
    public static void main(String[] args) {
        // BAD approach — tightly coupled to EmailSender
        NotificationServiceBad badService = new NotificationServiceBad();
        badService.notifyUser("alice@example.com", "Your order has shipped.");

        System.out.println("---");

        // GOOD approach — swap the sender without touching NotificationServiceGood
        NotificationServiceGood emailNotifier = new NotificationServiceGood(new EmailMessageSender());
        NotificationServiceGood smsNotifier   = new NotificationServiceGood(new SmsMessageSender());

        emailNotifier.notifyUser("alice@example.com", "Your order has shipped.");
        smsNotifier.notifyUser("+1-555-0199",         "Your order has shipped.");
    }
}
Output
Sending email to alice@example.com | Subject: Notification
---
[Email] To: alice@example.com | Your order has shipped.
[SMS] To: +1-555-0199 | Your order has shipped.
Pro Tip: The Design Rule Senior Engineers Live By
'Favour composition over inheritance' is from the Gang of Four's Design Patterns book (1994) and it's still the most misunderstood advice in Java. It doesn't mean 'never use inheritance' — it means use inheritance only when a genuine IS-A relationship exists. If your main motivation is code reuse, reach for composition and interfaces first.
Production Insight
A production system I worked on had a three-level inheritance hierarchy for payment processing. The base class changed a method signature for a new compliance requirement, and it broke 15 child classes across 5 microservices. The deployment was rolled back, but the incident cost two days of work for the team.
The real problem was that two of the child classes didn't actually have an IS-A relationship — they just needed the same validation logic and used inheritance for convenience. That's the fragile base class problem in action.
Rule: if you're extending for code reuse alone, stop. Use composition or a utility class instead.
Key Takeaway
Apply the IS-A test before every use of extends — if it sounds wrong, use composition.
Composition gives you runtime flexibility, loose coupling, and prevents the fragile base class problem.
Interfaces provide polymorphic behaviour without the tight coupling of inheritance — use them as contracts.

When NOT to Use Inheritance — Production Scenarios Where Composition Wins

While the previous section covered the theoretical IS-A test, here are concrete production scenarios where you must avoid inheritance and choose composition instead.

1. When you need runtime flexibility — If you might need to swap the behaviour at runtime (e.g., different payment gateways), composition with an interface is the only clean way.

2. When the parent class changes frequently — A volatile base class will cause constant breakage across child classes. Use composition to insulate dependent classes.

3. When you only want code reuse — If there is no IS-A relationship, inheritance is the wrong tool. Use composition or a utility class.

4. When the hierarchy deepens — Beyond 3 levels, refactor to composition. Deep hierarchies are impossible to reason about in production debugging.

5. When you want to mock behaviour in tests — Inheritance makes mocking harder because you often need to subclass the real class. Composition with interfaces allows easy mocking.

Example scenario: A team built a notification system where every channel (Email, SMS, Push) inherited from a base NotificationChannel. Later they needed to add a Slack channel that used a completely different API. The base class's protected methods leaked internal state, forcing the Slack implementation to override many methods it didn't need. Refactoring to composition — each channel implementing a MessageSender interface — eliminated the fragile coupling overnight.

RefactorToCompositionDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Instead of extending a heavy base class, compose with an interface

interface MessageSender {
    void send(String recipient, String message);
}

class EmailSender implements MessageSender {
    @Override
    public void send(String recipient, String message) {
        System.out.println("[Email] " + recipient + ": " + message);
    }
}

class SmsSender implements MessageSender {
    @Override
    public void send(String recipient, String message) {
        System.out.println("[SMS] " + recipient + ": " + message);
    }
}

class SlackSender implements MessageSender {
    @Override
    public void send(String channel, String message) {
        System.out.println("[Slack] #" + channel + ": " + message);
    }
}

class NotificationService {
    private MessageSender sender;  // composition

    public NotificationService(MessageSender sender) {
        this.sender = sender;
    }

    public void notify(String recipient, String msg) {
        sender.send(recipient, msg);
    }
}
Production Insight
A production system I audited had a 7-level hierarchy for UI components. Any change to the base component required testing all 7 levels. The team spent three weeks refactoring to composition using a component pattern. The result: deployment times dropped by 40% and bugs related to unexpected inheritance behaviour disappeared. If you see hierarchy depth > 3 in a code review, flag it as a design smell.
Key Takeaway
When in doubt, prefer composition. It gives you flexibility, testability, and avoids fragile base class issues. Use inheritance only when the IS-A relationship is crystal clear and the hierarchy is shallow.

Inheritance Best Practices — Patterns That Survive Production

After years of debugging inheritance disasters, here are the patterns that actually work in production.

1. Keep inheritance hierarchies shallow. More than 3 levels deep is a red flag. Deep hierarchies make it impossible to understand which version of a method runs without tracing through every level. Senior engineers refactor deep trees into composition.

2. Design parent classes for extension — or prohibit it. Mark classes as final if they're not designed to be inherited. This is a strong signal to the next developer. Similarly, mark methods as final if they should never be overridden (like critical security checks).

3. Use the Template Method pattern. Define a skeleton algorithm in the parent class with abstract steps, and let child classes implement those steps. This keeps control with the parent while allowing variation. Spring's JdbcTemplate is a famous example.

4. Never call overridable methods from a constructor. As discussed earlier, the child object isn't fully initialised when the parent constructor runs. If the overridden method depends on child state, you'll get null or unexpected behaviour.

5. Document the contract. Use Javadoc to specify the purpose of each method, especially which ones are designed to be overridden and what the override must guarantee (e.g., always call super).

TemplateMethodDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// Template Method Pattern — parent controls the algorithm, child fills in details

abstract class DataExporter {
    // Template method — defines the skeleton
    public final void export(String data) {
        validate(data);            // step 1: always validate
        transform(data);           // step 2: transform (abstract)
        write(data);               // step 3: write (abstract)
        cleanup();                 // step 4: cleanup (optional hook)
    }

    protected abstract String transform(String data);
    protected abstract void write(String data);

    private void validate(String data) {
        if (data == null || data.isEmpty()) {
            throw new IllegalArgumentException("Data cannot be empty");
        }
        System.out.println("Data validated.");
    }

    // Hook method — optional override
    protected void cleanup() {
        // default: do nothing
    }
}

class CsvExporter extends DataExporter {
    @Override
    protected String transform(String data) {
        return data.replace(",", ";");
    }

    @Override
    protected void write(String data) {
        System.out.println("Writing CSV: " + data);
    }
}

class JsonExporter extends DataExporter {
    @Override
    protected String transform(String data) {
        return "{\"data\":\"" + data + "\"}";
    }

    @Override
    protected void write(String data) {
        System.out.println("Writing JSON: " + data);
    }

    @Override
    protected void cleanup() {
        System.out.println("Closing JSON writer.");
    }
}
Template Method Pattern in Action
The parent's export() method is declared final so subclasses can't change the algorithm order. Child classes only implement the abstract steps. This is a clean use of inheritance — the parent provides structure, children provide variation.
Production Insight
A team used the Template Method pattern for a data export pipeline. A developer accidentally overrode the export method in a child class, skipping validation. Data corruption went unnoticed for weeks because logs showed successful exports. The fix: mark the template method as final and enforce code review. This pattern reduces inheritance misuse by limiting what children can change.
Key Takeaway
Use Template Method when you want to reuse algorithm structure while allowing variation in specific steps.
Mark template methods final to prevent accidental override of the skeleton.
Keep hierarchies shallow and document the contract for overridable methods.

What You Can Actually Do in a Subclass — and What You Can't

Inheritance isn't a free-for-all. When you slap extends on a class, you get access to public and protected members — but private members stay locked in the superclass. Period. The subclass can't see them, touch them, or override them. If you need a subclass to modify private state, you expose it through protected getters and setters, or you rethink your design.

You can add new fields, new methods, override existing ones, or hide static methods (don't hide static methods — that's a code smell 9 times out of 10). You cannot shrink the visibility of an overridden method — if the parent says protected, your override can't go private. That breaks the Liskov Substitution Principle, and your compiler will slap you.

Constructors are not inherited. If the superclass only has a parameterized constructor, your subclass must call it via super(...) as the first statement in its own constructor. Miss that, and the compiler refuses to compile. This isn't a suggestion — it's a hard rule enforced at compile time.

PrivateFieldAccess.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// io.thecodeforge — java tutorial

class PaymentGateway {
    private String apiKey = "sk_live_secret";
    protected String endpoint = "https://api.stripe.com/v1";

    private void logKey() {
        System.out.println("Logging key (private method)");
    }

    protected void charge(int amount) {
        System.out.println("Charging $" + amount);
        logKey();
    }
}

class StripeGateway extends PaymentGateway {
    // Cannot access apiKey or logKey() — they're private
    // This override is fine: same or wider visibility
    @Override
    public void charge(int amount) {
        System.out.println("Stripe charging $" + amount + " via " + endpoint);
        // super.charge(amount); // would call private logKey internally
    }

    // Compiler error: attempting to reduce visibility
    // @Override
    // private void charge(int amount) { }
}

public class Main {
    public static void main(String[] args) {
        StripeGateway gw = new StripeGateway();
        gw.charge(2999);
    }
}
Output
Stripe charging $2999 via https://api.stripe.com/v1
Production Trap:
Never make a field protected just so a subclass can access it directly. That breaks encapsulation. Use protected accessor methods. I've seen a protected field cascade into a tangled mess of mutations across five subclasses. Don't be that dev.
Key Takeaway
Subclasses inherit public and protected members. Private members stay hidden. Constructors are not inherited — call super() explicitly or prepare for compile errors.

Multiple Inheritance Through Interfaces — Java's Boring but Safe Escape Hatch

Java doesn't allow multiple class inheritance. A class can't extend two classes. Why? Because of the Diamond Problem — if two parent classes define the same method, which one does the child inherit? C++ says good luck, Java says no thanks.

But you can implement multiple interfaces. That's Java's compromise. An interface defines a contract — no state, just abstract methods (before Java 8) or default methods (since Java 8). If two interfaces define a default method with the same signature, the implementing class must override it, or the compiler screams. That's the diamond problem handled explicitly, not silently.

Real-world example: an InvoiceService can implement both Exportable and Auditable. Each interface declares a behavior. The class implements both, and if there's a conflict between default methods — export() returning CSV vs JSON — you resolve it in the class. It's verbose. It's safe. It's Java.

InterfaceMultipleInheritance.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// io.thecodeforge — java tutorial

interface Exportable {
    default String export() {
        return "CSV export";
    }
}

interface Auditable {
    default String export() {
        return "Audit log export";
    }
}

class InvoiceService implements Exportable, Auditable {
    // Must resolve conflict — no silent ambiguity
    @Override
    public String export() {
        // Pick one, or combine both
        return Exportable.super.export() + " + " + Auditable.super.export();
    }

    public void process() {
        System.out.println("Processing invoice...");
        System.out.println("Exported: " + export());
    }
}

public class Main {
    public static void main(String[] args) {
        new InvoiceService().process();
    }
}
Output
Processing invoice...
Exported: CSV export + Audit log export
Senior Shortcut:
Prefer interfaces with default methods over abstract classes when you need shared behavior across unrelated classes. Abstract classes lock you into a hierarchy. Interfaces let you mix behaviors without the coupling. Save extends for 'is-a' relationships, not 'has-a'.
Key Takeaway
Java prohibits multiple class inheritance to avoid the Diamond Problem. Use interfaces for multiple behaviors — resolve default method conflicts explicitly, never silently.
● Production incidentPOST-MORTEMseverity: high

When a Parent Method Change Broke 30 Child Classes in Production

Symptom
After deploying a seemingly minor update to the base DocumentHandler class, several document processing pipelines started throwing ClassCastException or silently failing to save documents. Logs showed 'java.lang.VerifyError: Expecting a stackmap frame' or unexpected behaviour in overridden methods.
Assumption
The assumption was that changing the return type of a method that was not referenced by any child class would be safe — the team believed child classes only depended on the method's side effects.
Root cause
The parent's method signature changed, but child classes that overrode the method still had the old signature. Java's method dispatch uses the exact signature at compile time; at runtime, the JVM found a mismatch and threw a linkage error. Additionally, some child classes used @Override which caught the mismatch — but two classes had no @Override and silently created new methods, causing those pipelines to call the parent version instead of their intended logic.
Fix
Reverted the deployment. Used the @Override annotation on every override in the codebase (added a checkstyle rule). Then changed the parent method to add a new overloaded method instead of modifying the existing one, preserving backward compatibility. The new method was added with a default implementation that called the old method, and child classes were updated over two sprints.
Key lesson
  • Never change the signature of a public or protected method in a parent class without a full audit of all child classes.
  • Always use @Override on every overridden method — it's a compile-time safety net against signature drift.
  • Prefer adding new overloaded methods over modifying existing ones to preserve backward compatibility.
  • Use the Open/Closed Principle: parent classes should be open for extension but closed for modification once released.
Production debug guideCommon symptoms and immediate actions when inheritance behaviour breaks4 entries
Symptom · 01
Overridden method is not being called; parent method runs instead
Fix
Check if the child class actually overrides the method: verify signature matches exactly (including return type). Ensure @Override is present — without it, a typo creates a new method, not an override. Also check if the method is private (private methods are not inherited).
Symptom · 02
Constructor of child class fails to compile with 'implicit super constructor is undefined'
Fix
The parent class does not have a no-argument constructor. Add an explicit super(…​) call in the child constructor with matching arguments, or add a no-arg constructor to the parent class.
Symptom · 03
Child class is not visible or type mismatch in polymorphic assignment
Fix
Check access modifiers: if the child class is package-private and the parent is public, you cannot reference the child from outside the package. Also verify the inheritance hierarchy — the child must directly or transitively extend the parent.
Symptom · 04
Method throws UnsupportedOperationException when called on a parent reference
Fix
The parent method might have a default implementation that throws, but the child is supposed to override it. Check all child classes for missing overrides. Use instanceof checks in debug logs to verify the runtime type.
★ Inheritance Debugging Cheat SheetQuick commands and checks for common inheritance pitfalls
Overridden method not called
Immediate action
Add @Override to the child method and recompile — the compiler will flag signature mismatch.
Commands
javap -c -p ParentClass.class | grep methodName — check actual method signature in bytecode
Add logging: System.out.println("Running parent version") inside both parent and child methods to see which is invoked.
Fix now
Correct the method signature in the child to exactly match the parent (including return type). Ensure the method is not private in the parent.
Parent constructor not executed+
Immediate action
Verify the child constructor calls super(args) as the very first statement. No other code before super().
Commands
Add a print statement in the parent constructor and child constructor to confirm execution order.
Check if parent class has a no-arg constructor; if not, the child must explicitly call super(…​).
Fix now
Add super(requiredArgs) as first line in child constructor. If parent has no no-arg constructor, add one (or always call super explicitly).
Polymorphic call invokes wrong method after refactoring+
Immediate action
Check if any child classes lost their @Override after refactoring. Run a grep for 'implements' and 'extends' to verify hierarchy.
Commands
Use IDE feature 'Show Hierarchy' (Ctrl+H in Eclipse, Ctrl+T in IntelliJ) to visualise overrides.
Run 'mvn compile' or 'gradle compileJava' with -Xlint:all to get warnings about missing @Override.
Fix now
Add @Override annotation. If the method was removed from parent, add it back or change the child to match the new hierarchy.
Inheritance vs Composition
AspectInheritanceComposition
RelationshipIS-A (tight coupling)HAS-A (loose coupling)
Code reuseReuse via extension, limited to one parentReuse via delegation, flexible and composable
Runtime flexibilityFixed at compile timeCan swap behaviours at runtime via dependency injection
TestabilityHarder to mock, often requires subclassingEasy to mock by injecting mock implementations
EncapsulationLeaks parent implementation details via protected membersEncapsulates behaviour behind interfaces
FragilityFragile base class problem — parent changes break childrenChanges to composed objects are isolated
PolymorphismNatural via method overridingAchieved through interfaces and delegation

Key takeaways

1
Inheritance models an IS-A relationship; if it doesn't sound right, use composition.
2
Always add @Override to every overriding method
it's your compile-time safety net.
3
Parent constructors run first; super() must be the first statement in a child constructor.
4
Dynamic dispatch ensures the correct overridden method runs based on the actual runtime type.
5
Keep inheritance hierarchies shallow (max 3 levels) to avoid the fragile base class problem.
6
Prefer composition over inheritance for code reuse unless a genuine IS-A relationship exists.
7
Sealed classes (Java 17+) let you control which classes can extend a parent, reducing unexpected subclasses.

Common mistakes to avoid

5 patterns
×

Forgetting @Override annotation

Symptom
Child method silently becomes a new method instead of overriding the parent. Polymorphic calls invoke the parent logic, leading to incorrect behaviour or missing functionality.
Fix
Always add @Override to methods intended to override. Configure your IDE to warn if @Override is missing and enforce it in CI with Checkstyle or SpotBugs.
×

Calling overridable methods from a constructor

Symptom
When the overridden method is invoked during object construction, it may depend on child class fields that are not yet initialised, resulting in NullPointerException or unexpected default values.
Fix
Never call overridable methods in constructors. If you must call such logic, use a factory method or postpone initialization to an init() method called after construction.
×

Using inheritance for code reuse when no IS-A relationship exists

Symptom
Brittle hierarchy: a change in the parent class breaks multiple unrelated child classes. Testing and debugging become harder because inherited methods expose unintended behaviour.
Fix
Apply the IS-A test. If the relationship is not clear, use composition (HAS-A) instead. Extract shared logic into a utility class or inject via an interface.
×

Removing or changing a protected method/field in a parent class without auditing children

Symptom
Child classes fail to compile if recompiled, or worse, runtime errors like NoSuchMethodError or IncompatibleClassChangeError when only the parent JAR is updated.
Fix
Before changing protected members, audit all known child classes. Prefer adding new overloaded methods over modifying existing ones. Use a deprecation cycle with @Deprecated before removal.
×

Not using the @Override annotation when overriding

Symptom
If the parent method signature changes (e.g., parameter type), the child method no longer overrides it and becomes an independent method. Polymorphic calls stop working as expected.
Fix
Always use @Override. It causes a compile error if the method does not actually override a parent method, catching signature drift early.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between method overloading and method overriding ...
Q02SENIOR
Why does Java not support multiple inheritance of classes?
Q03SENIOR
Explain the 'fragile base class' problem and how to mitigate it in produ...
Q01 of 03JUNIOR

What is the difference between method overloading and method overriding in Java?

ANSWER
Overloading occurs when multiple methods in the same class have the same name but different parameter lists. It is resolved at compile time (static polymorphism). Overriding occurs when a subclass provides a specific implementation of a method already defined in its superclass. It is resolved at runtime based on the object's actual type (dynamic polymorphism). Overriding requires the same signature and return type (or covariant return type) and cannot be done on private, static, or final methods. Overloading does not have these restrictions.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
Can a private method be overridden in Java?
02
What is the difference between 'super' and 'this' in the context of inheritance?
03
Is it possible to extend a final class?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's OOP Concepts. Mark it forged?

10 min read · try the examples if you haven't

Previous
Constructors in Java
3 / 16 · OOP Concepts
Next
Polymorphism in Java