Java Inheritance

Java Inheritance: How the extends Keyword Works and When to Avoid It

A one-line change to a shared base class — adding an audit log call, tightening a validation check — ships clean through code review and breaks forty tests overnight. Nobody touched those tests. Java inheritance is what connects them: every subclass silently depends on exactly how its parent behaves, and that dependency is invisible until a change ripples through it. Java inheritance lets a subclass reuse and extend a parent class’s fields and methods, using the extends keyword — simple syntax hiding a real architectural risk. This guide covers how inheritance actually works, why deep hierarchies become fragile, and when composition is the better call.

What Is Java Inheritance?

Inheritance lets one class acquire the fields and methods of another, expressing an “is-a” relationship — a CreditCardTransaction is a Transaction, a ChromeDriver is a WebDriver. Java supports only single inheritance for classes: a class can extend exactly one parent, though it can implement multiple interfaces. Not everything from the parent transfers automatically.

Member Inherited? Note
public / protected fields & methods Yes Directly accessible in the subclass
private fields & methods No Exist in the object but not directly accessible by name
Constructors No Must be invoked via super(), not inherited directly — see Java constructors
static members Shared, not overridden Resolved by declared type, not the runtime object

Single Inheritance and Why Java Skips the Diamond Problem

Languages that allow a class to extend multiple parents run into the diamond problem: if two parent classes both define a method with the same signature, which one does the subclass inherit? Oracle’s official Java inheritance documentation confirms Java sidesteps this entirely by restricting classes to single inheritance. Multiple behaviors still come from multiple sources through interfaces, which can each define default methods — but a class implementing two interfaces with conflicting default methods is forced to resolve the conflict explicitly, rather than leaving it to silent, ambiguous resolution.

The Fragile Base Class Problem

The fragile base class problem is a well-documented risk in object-oriented design: a change to a base class that looks completely safe in isolation can break subclasses that depended on the base class’s exact prior behavior, not just its documented contract. The base class author has no way to see every subclass that exists, and the subclass author has no way to know which of the base class’s behaviors their code is silently relying on.

BaseTransactionProcessor
one small change here

 

CreditCardProcessor

ACHProcessor

WireTransferProcessor

All three subclasses inherit the change automatically
even the ones whose tests never touched the modified code path

Where This Shows Up in QA, BA, and IT Work

Page Object Base Class Changes Breaking Dozens of Tests

Selenium frameworks commonly build a base Page Object class with shared wait logic, navigation helpers, and element interaction methods, with every page-specific class extending it. A single change to that base class’s wait timing or element-finding logic can ripple through every subclass simultaneously — the exact scenario covered in the defect below.

Legacy Entity Class Hierarchies

Healthcare and financial systems built over many years frequently accumulate deep inheritance chains in their data model — a Person base class extended by Patient, extended by InpatientRecord, extended further still. Each additional layer makes the fragile base class problem worse, since a change at the top of the chain now has to be traced through every intermediate layer to understand its full blast radius.

Test Data Builder Inheritance Chains

Test data builders sometimes inherit from each other to share setup logic — a BaseClaimBuilder extended by DeniedClaimBuilder, extended by AppealedClaimBuilder. This works until a change meant for one specific scenario in the base builder silently alters test data for every builder further down the chain.

Composition vs. Inheritance: The Real Decision

Joshua Bloch’s Effective Java — widely regarded as one of the field’s standard references — famously argues for favoring composition over inheritance except when a genuine, stable “is-a” relationship exists. Composition means a class holds a reference to another class and delegates to it, rather than extending it directly.

Approach Coupling Best For
Inheritance Tight — subclass depends on parent’s internal behavior A genuine, stable is-a relationship (ChromeDriver is-a WebDriver)
Composition Loose — depends only on a documented interface Code reuse without a true is-a relationship (a class that “has-a” wait helper)

The test worth applying before reaching for extends: can you honestly say the subclass is a more specific version of the parent, or are you just trying to reuse some convenient methods? The first case is inheritance. The second is composition wearing the wrong syntax.

Scenario: Financial IT — An Audit Log Change Breaks Twelve Processors Overnight

A compliance requirement adds a mandatory audit log entry to BaseTransactionProcessor.process(), the shared parent class for every payment type in the system. The change passes code review — it’s three lines, clearly correct in isolation.

TFF-3810Critical

Summary: 12 of 14 payment processor subtypes failing after audit log change to base class

Steps to Reproduce: Run full regression suite against any payment processor subtype after the merge.

Expected: Audit log entry added; all processors continue functioning normally.

Actual: 12 of 14 processor subtypes throw timeout exceptions; 2 unaffected.

Environment: Staging, full processor regression suite

Investigation found that most subclasses overrode process() and called super.process() partway through their own logic — not at the start — because the base class’s original method order happened to work with each subclass’s assumptions about timing. The new audit log call, inserted at the top of the base method, now ran before several subclasses had finished setting up state the log call depended on, causing null references deep in the logging pipeline that manifested as timeouts.

Nothing about the change violated the base class’s documented contract — there wasn’t one specifying call order guarantees that precisely. The fix documented an explicit contract for where in process() subclasses may safely call super.process(), and added a base-class-level integration test running against every subclass, specifically to catch this exact ripple effect before the next base class change ships.

Scenario: QA Automation — A Page Object Base Class Refactor Breaks 40 Tests

A team refactors their base BasePage class to add a retry mechanism around element waits, intending to reduce flaky failures. Instead, the change immediately breaks 40 of the suite’s 180 tests — all of them page types that had overridden the base wait method with page-specific timing logic.

Before (fragile inheritance):

class BasePage {
    void waitForLoad() { /* generic wait logic */ }
}
class DashboardPage extends BasePage {
    @Override
    void waitForLoad() { /* custom timing, ignores parent entirely */ }
}

After (composition-based fix):

class WaitHelper {
    void waitForElement(WebElement el, Duration timeout) { /* shared logic */ }
}
class DashboardPage {
    private final WaitHelper waits = new WaitHelper();
    void waitForLoad() { waits.waitForElement(loadingSpinner, Duration.ofSeconds(15)); }
}

Moving shared wait logic into a standalone WaitHelper class that pages hold a reference to, rather than inherit from, means a future change to retry behavior only affects pages that explicitly opt into calling the new logic. No page can silently break because of an override interaction nobody remembered was there — the exact failure mode both scenarios in this article share.

Common Mistakes and Edge Cases

Using inheritance purely for code reuse. If the only reason a class extends another is to avoid retyping a few methods, and no genuine is-a relationship exists, that’s the composition-over-inheritance test failing — and it’s usually a sign the shared logic belongs in a standalone helper class instead.

Deep inheritance chains, four levels or more. Every additional layer multiplies the number of places a behavior could originate from, making a defect harder to trace back to its actual source. A method call three levels up an unfamiliar hierarchy is a common reason code review takes longer than it should.

Overriding without understanding base class invariants. As the financial scenario shows, a base class can have implicit assumptions — about call order, about what state exists by the time a method runs — that were never written down anywhere, and an override that doesn’t honor them can compile perfectly while still breaking.

Exposing protected fields across a hierarchy. protected fields are directly accessible to every subclass, which feels convenient until five subclasses are all reading and writing the same field with different assumptions about its current state — a well-known erosion of encapsulation that composition avoids by default.

When I’d Use Inheritance vs. Composition

Inheritance: when the is-a relationship is genuinely stable and unlikely to change — a browser driver implementing a standard interface, a specific exception type extending a general one. Selenium’s WebDriver hierarchy, covered in more depth in this site’s polymorphism and extensibility guide, is a well-behaved example.

Composition: the default choice whenever you’re reusing behavior without a true is-a relationship, or when the shared logic is likely to need different variations across the classes that use it. It costs a little more boilerplate upfront and saves considerably more debugging time later.

Quick Reference Checklist by Role

QA / Test Automation Engineer

  • Run the full regression suite against every subclass before merging any shared base class change
  • Flag Page Object hierarchies deeper than 2-3 levels for a composition-based refactor
  • Check whether a sudden multi-test failure traces back to one shared parent class
Business Analyst

  • Ask how many subclasses a proposed base class change actually affects before estimating impact
  • Treat “unrelated feature broke” reports as a possible shared-inheritance ripple effect
  • Flag legacy entity hierarchies deeper than 3 levels as technical debt worth documenting
IT Support / Ops Analyst

  • Check recent shared base class changes first when multiple unrelated features fail simultaneously
  • Recognize widespread, same-shaped failures across subtypes as a possible base class regression
  • Request a documented call-order contract for any base class method subclasses override

The next time one small change to a shared class breaks features that never should have been connected, don’t just fix the immediate symptom. Check whether that class is being extended for a genuine is-a relationship or just borrowed for convenience — and if it’s the latter, that ripple effect will happen again until the inheritance becomes composition.


Further reading: Oracle’s official Java inheritance tutorial covers the full language specification. For interface-based design as a composition-friendly alternative, see Oracle’s official interfaces tutorial.

Download the Inheritance vs. Composition Decision Checklist (PDF)

A one-page decision guide for choosing inheritance vs. composition, plus a base-class change impact checklist for code review.

Get the Free Checklist →

Scroll to Top