Java Inheritance — Parent Method Change Broke 30 Classes
Parent method change broke 30 child classes with VerifyError in production.
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- 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
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.
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.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.
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.
| Advantage | Disadvantage |
|---|---|
| Code reusability — write once, use in all subclasses | Tight coupling — changes in parent can break children |
| Polymorphism — treat different objects uniformly via parent reference | Fragile base class — parent changes require auditing all children |
| Logical hierarchy — models real-world IS-A relationships | Single inheritance limitation — can't inherit from multiple classes |
| Easy extension — add new subclasses without modifying existing code | Deep hierarchies become complex and hard to debug |
| Method overriding enables custom behavior | Can expose implementation details via protected members |
| Built into Java language — no extra libraries needed | May 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.
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.
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.
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.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 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()
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: 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.super()
super() must be the first statement — the compiler enforces what good design demands.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.super() must be the first statement.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.
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.
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).
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.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.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.
protected field cascade into a tangled mess of mutations across five subclasses. Don't be that dev.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 — returning CSV vs JSON — you resolve it in the class. It's verbose. It's safe. It's Java.export()
When a Parent Method Change Broke 30 Child Classes in Production
- 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.
instanceof checks in debug logs to verify the runtime type.javap -c -p ParentClass.class | grep methodName — check actual method signature in bytecodeAdd logging: System.out.println("Running parent version") inside both parent and child methods to see which is invoked.| File | Command / Code | Purpose |
|---|---|---|
| BankAccountDemo.java | class BankAccount { | What Inheritance Actually Does |
| InheritanceTypesDemo.java | class Animal { | Types of Inheritance in Java |
| io | sealed interface Shape permits Circle, Rectangle, Triangle {} | Using instanceof Pattern Matching with Sealed Classes (Java |
| ShippingCalculatorDemo.java | class ShippingCarrier { | Method Overriding |
| EmployeeHierarchyDemo.java | class Employee { | The super Keyword and Constructor Chaining |
| CompositionVsInheritanceDemo.java | class EmailSender { | When NOT to Use Inheritance |
| RefactorToCompositionDemo.java | interface MessageSender { | When NOT to Use Inheritance |
| TemplateMethodDemo.java | abstract class DataExporter { | Inheritance Best Practices |
| PrivateFieldAccess.java | class PaymentGateway { | What You Can Actually Do in a Subclass |
| InterfaceMultipleInheritance.java | interface Exportable { | Multiple Inheritance Through Interfaces |
Key takeaways
super() must be the first statement in a child constructor.Interview Questions on This Topic
What is the difference between method overloading and method overriding in Java?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
That's OOP Concepts. Mark it forged?
8 min read · try the examples if you haven't