Inheritance in Java

Master Java inheritance: extends keyword, super, method overriding, and substitutability, with practical guidance for reliable class hierarchies.

published: reading time: 18 min read author: Geek Workbench
Quick Summary

Inheritance lets a subclass reuse a parent implementation, but each subtype must still honor the promises callers rely on. This guide explains constructor calls, overriding, sealed classes, and the Liskov Substitution Principle, with examples of failures caused by fragile base classes and skipped lifecycle work. Use its checklists to decide when an inheritance hierarchy fits and when composition gives you safer flexibility.

Inheritance in Java

Introduction

Inheritance is the mechanism that lets you build class hierarchies where subclasses automatically receive fields and methods from their ancestors — code reuse through a “is-a” relationship.

When to Use

Use inheritance when:

  • A true “is-a” relationship exists — a Dog is an Animal
  • Shared behavior is identical — subclasses should use the exact same implementation
  • You need polymorphism — treating different types through a common supertype
  • Designing for extension — allowing subclasses to customize behavior via overriding
// Parent class
public class Animal {
    protected String name;
    protected int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void eat() {
        System.out.println(name + " is eating");
    }

    public void sleep() {
        System.out.println(name + " is sleeping");
    }
}

// Child class
public class Dog extends Animal {
    private String breed;

    public Dog(String name, int age, String breed) {
        super(name, age);  // Call parent constructor
        this.breed = breed;
    }

    @Override
    public void eat() {
        System.out.println(name + " (a " + breed + ") is eating kibble");
    }

    public void bark() {
        System.out.println(name + " says Woof!");
    }
}

When Not to Use

Avoid inheritance when:

  • “Has-a” relationship — a Car has an Engine, not is an Engine
  • Different implementations for same behavior — use interfaces instead
  • Unrelated classes — don’t force hierarchy where none exists
  • Multiple inheritance needed — Java only supports single class inheritance, use interfaces
// Bad: forced inheritance
public class Stack extends ArrayList {  // Stack is not an ArrayList!
    // Inappropriate — ArrayList has too much functionality
}

// Good: composition
public class Stack {
    private List<String> items = new ArrayList<>();  // Has-a relationship
}

Inheritance Hierarchy — Mermaid Diagram

classDiagram
    class Animal {
        +String name
        +eat() void
        +sleep() void
    }
    class Dog {
        +String breed
        +bark() void
        +eat() void
    }
    class Cat {
        +boolean indoor
        +meow() void
        +eat() void
    }
    class Bird {
        +boolean canFly
        +chirp() void
    }
    Animal <|-- Dog
    Animal <|-- Cat
    Animal <|-- Bird
    note for Dog "extends — inherits name, age,\neat(), sleep() from Animal"

Failure Scenarios

1. Breaking the Liskov Substitution Principle

public class Rectangle {
    protected double width;
    protected double height;

    public void setWidth(double w) { width = w; }
    public void setHeight(double h) { height = h; }
}

public class Square extends Rectangle {
    // Violation: a Square cannot independently set width and height
    @Override
    public void setWidth(double w) {
        width = w;
        height = w;  // Must keep them equal
    }

    @Override
    public void setHeight(double h) {
        width = h;
        height = h;
    }
}

// If code expects Rectangle, Square breaks it:
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(3);  // Not a square anymore! But stored width is now 3 too

2. Forgetting to Call super()

public class Parent {
    public Parent(String name) {
        System.out.println("Parent constructed: " + name);
    }
}

public class Child extends Parent {
    public Child() {
        // super(); // IMPLICIT — but only if Parent has no-arg constructor
        // If Parent has only Parent(String), this won't compile
    }
}

3. Methods That Cannot Be Overridden

Java uses runtime dispatch for overridable instance methods, resolving the implementation from the actual object type. A subclass can replace an inherited method unless the method is final; the compiler rejects an override of a final method.

For example, Object.getClass() is final, so a subclass cannot make an object report a different runtime Class. Marking a method final also tells subclass authors that its implementation is part of the parent contract. The compiler enforces that boundary:

public class Base {
    public final void finalMethod() {
        System.out.println("Base behavior");
    }
}

public class Child extends Base {
    @Override
    public void finalMethod() {  // Compile error: cannot override a final method
        System.out.println("Child behavior");
    }
}

Production Failure Scenarios

A base class can provide logging, retries, and resource cleanup, but a subclass can lose those guarantees if it overrides the public workflow method.

class Job {
    public void run() {
        System.out.println("Starting job");
        try {
            execute();
        } finally {
            releaseLock();
            System.out.println("Job finished");
        }
    }

    protected void execute() {}

    protected void releaseLock() {
        // Release the distributed lock
    }
}

class ImportJob extends Job {
    @Override
    public void run() {
        importRows(); // Skips the parent's lock cleanup and lifecycle logging
    }

    private void importRows() {
        // Import data
    }
}

In production, the import may appear successful while the distributed lock remains held. Later jobs then stall, and operators see growing queue delays rather than an obvious failure. Keep the workflow in a final template method and expose a protected hook such as execute() for subclasses. Add a test that runs each job subtype and verifies cleanup happens after both success and exceptions.

Another warning sign is a subtype that throws UnsupportedOperationException for a method promised by its superclass. Callers typed to the parent can then fail only for certain runtime subtypes. Split the parent contract into smaller interfaces, or use composition when the subtype cannot honor the full contract.

Trade-off Table

Inheritance Type Code Reuse Flexibility Coupling
Class inheritance (extends) Full Moderate Tight — subclass depends on parent
Interface implementation None (just contracts) High Low — depends only on contract
Composition Delegated High Low — depends on collaborator interface
Default method (interface) Partial Moderate Moderate

Code Snippets

Method Overriding with @Override

public class Vehicle {
    protected double speed;

    public void move() {
        System.out.println("Vehicle moving at " + speed + " km/h");
    }

    public double getSpeed() {
        return speed;
    }
}

public class Car extends Vehicle {
    private int wheels = 4;

    @Override  // Compiler checks this is actually overriding something
    public void move() {
        System.out.println("Car driving at " + speed + " km/h with " + wheels + " wheels");
    }

    public void honk() {
        System.out.println("Car honking!");
    }
}

Using super to Access Parent Members

public class Employee {
    protected String name;
    protected double salary;

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

    public void work() {
        System.out.println(name + " is working");
    }

    public String getInfo() {
        return name + " earns " + salary;
    }
}

public class Manager extends Employee {
    private int teamSize;

    public Manager(String name, double salary, int teamSize) {
        super(name, salary);  // Call parent constructor
        this.teamSize = teamSize;
    }

    @Override
    public void work() {
        super.work();  // Call parent's work() first
        System.out.println(name + " is also managing " + teamSize + " people");
    }

    @Override
    public String getInfo() {
        return super.getInfo() + " (manages " + teamSize + ")";  // Extend parent info
    }
}

Sealed Classes and Controlled Inheritance

Java 17 sealed classes let a parent name the types allowed to extend it. This is useful when a domain has a small, known set of variants and callers should handle every case explicitly.

public sealed interface Payment permits CardPayment, CashPayment {}

public final class CardPayment implements Payment {}
public final class CashPayment implements Payment {}

Each permitted subtype must declare itself final, sealed, or non-sealed. Use final when the variant should stop there; choose non-sealed only when downstream extension is part of the design. Sealed types make the hierarchy easier to reason about, but they can make plugins or independently maintained extensions harder to support.

Observability Checklist

  • Inheritance only for true “is-a” relationships
  • @Override annotation used on all overriding methods
  • super() called explicitly when parent has no default constructor
  • Parent methods accessible via super when needed
  • Liskov Substitution Principle respected — subclasses work wherever parents are expected

Security Notes

  • Don’t expose internal state via protected fields — use protected methods
  • Validate in subclass — don’t trust parent invariants alone
  • Immutable parent — if parent is immutable, subclass should be too
  • Constructor security — don’t let subclass constructors bypass parent initialization
public class SecureAccount {
    private final String accountId;
    private double balance;

    // Constructor sets required invariants
    public SecureAccount(String accountId, double initialBalance) {
        if (accountId == null || accountId.isEmpty()) {
            throw new IllegalArgumentException("Invalid account ID");
        }
        this.accountId = accountId;
        this.balance = initialBalance >= 0 ? initialBalance : 0;
    }
}

// Subclass must call super(), which validates accountId
public class SavingsAccount extends SecureAccount {
    private double interestRate;

    public SavingsAccount(String accountId, double initialBalance, double interestRate) {
        super(accountId, initialBalance);  // Parent validation runs first
        this.interestRate = interestRate;
    }
}

Security and Compliance Notes

Avoid calling an overridable authorization method from a superclass operation. Java dispatches that call to the runtime subtype, so a subclass can weaken the check even when the caller uses the superclass API.

class DocumentService {
    public byte[] read(User user, Document document) {
        if (!canRead(user, document)) {
            throw new SecurityException("Access denied");
        }
        return document.contents();
    }

    protected boolean canRead(User user, Document document) {
        return document.ownerId().equals(user.id());
    }
}

class SharedDocumentService extends DocumentService {
    @Override
    protected boolean canRead(User user, Document document) {
        return true; // Accidentally makes every document readable
    }
}

The symptom may be unauthorized reads that still pass through the normal service and appear in ordinary request logs. A compliance review can also find that the recorded policy differs from the policy actually applied. Keep authorization checks private or final, enforce policy in a dedicated component that cannot be overridden through inheritance, and test access through every exposed service subtype. Record the subject, resource, decision, and policy result in the audit trail without logging document contents or credentials.

Pitfalls

  1. Deep inheritance hierarchies — prefer shallow hierarchies (max 2-3 levels)
  2. Forcing inheritance where composition fits better — “has-a” vs “is-a”
  3. Breaking substitutability — subclass must work wherever parent is expected
  4. Overriding methods that weren’t designed for extension — the fragile base class problem
  5. Diamond inheritance — not possible in Java (use interfaces for multiple contracts)
// Common mistake: inheritance for code reuse rather than polymorphism
class MyStack extends ArrayList {  // Wrong — Stack is not an ArrayList
    public void push(Object item) { add(item); }
    public Object pop() { return remove(size() - 1); }
}

// Correct: composition
class MyStack {
    private List<Object> items = new ArrayList<>();
    public void push(Object item) { items.add(item); }
    public Object pop() { return items.remove(items.size() - 1); }
}

Common Pitfalls / Anti-Patterns

Extending a class only to reuse a few methods gives callers every public operation on the parent. For example, Stack extends ArrayList lets stack users insert at index zero or remove from the middle, so the type no longer guarantees last-in, first-out behavior. Prefer a stack that contains a private Deque and exposes only push, pop, and peek.

Watch for these inheritance traps:

  • Subclasses that weaken parent guarantees: a Square subtype cannot safely honor a Rectangle API that allows width and height to change independently. Use separate shape types or an interface with operations both types can support.
  • Overrides that skip parent work: if the superclass owns validation, cleanup, auditing, or transaction boundaries, a replacement method can silently bypass it. Keep the workflow fixed and expose narrow hooks.
  • Protected mutable state: subclasses can change parent data without going through its validation. Prefer private fields and protected methods with explicit invariants.
  • Deep hierarchies for small variations: each level adds another place where behavior can change. Use composition or a strategy object when behavior needs to vary independently.

When a proposed subtype needs exceptions to the parent contract, treat that as a design signal. Narrow the parent API or replace the inheritance relationship before callers have to learn which subclasses behave differently.

Quick Recap

  • extends = inheritance keyword for classes (single inheritance only)
  • super = reference to parent class members (constructor call super(), method call super.method())
  • @Override = annotation ensuring method actually overrides parent method
  • Subclass must call super() explicitly if parent has no default constructor
  • Liskov Substitution Principle = subclass objects must be usable wherever parent objects are expected
  • Favor composition over inheritance = “has-a” over “is-a” for flexibility

Quick Recap Checklist

  • Use extends only when the subclass is a valid substitute for its parent.
  • Call an accessible parent constructor with super(...) when no suitable no-argument constructor exists.
  • Mark overridden methods with @Override.
  • Prefer composition for “has-a” relationships or replaceable behavior.

Interview Questions

1. What is the difference between `this` and `super`?
`this` refers to the current object instance, used to access members of the same class. `super` refers to the parent class instance, used to access members that the current class inherited from its parent.
2. Can a class inherit from multiple classes in Java?
No, Java supports only single class inheritance. A class can implement multiple interfaces, but can only `extend` one class. This avoids the diamond problem but requires using interfaces for multiple contracts.
3. What is the difference between method hiding and method overriding?
Overriding replaces parent method implementation at runtime via virtual dispatch. Hiding applies to static methods — subclass static method hides parent static method. Hidden methods are resolved at compile time; overridden methods at runtime.
4. What is the fragile base class problem in inheritance hierarchies?
Fragile base class occurs when a parent class changes internally and breaks subclass behavior. Subclasses depend on parent implementation details, not just public contract. Solution: favor composition over inheritance, keep parent classes stable.
5. What access modifier allows inheritance but prevents external access?
`protected` — accessible within same package and by subclasses in any package. Used for members that should be inherited but not exposed publicly. Common for methods that subclasses may need to call or override.
6. What is the difference between a IS-A and HAS-A relationship?
IS-A represents inheritance — Dog IS AN Animal → class Dog extends Animal. HAS-A represents composition — Car HAS AN Engine → class Car { Engine engine; }. Use inheritance when true IS-A; use composition when HAS-A is more accurate.
7. Can you prevent a class from being subclassed?
Yes — declare class as `final` to prevent any subclassing. Or use sealed classes (Java 17+) with permits clause to control which classes can extend. Also, private constructors prevent external instantiation while allowing internal subclasses.
8. What is method resolution order in a class hierarchy?
JVM first checks static methods at compile time (method hiding). For instance methods, starts at actual runtime class, searches up to Object if not found. Constructors are special — they don't participate in virtual dispatch.
9. What happens when a subclass overrides a parent method with stricter visibility?
Compilation error — overriding methods cannot reduce visibility (can't go from public to protected). Liskov Substitution Principle requires subclass method be at least as accessible as parent method. Use @Override annotation to catch these violations at compile time.
10. What is the purpose of the `super` keyword in method overriding?
super.methodName() calls the parent class version of the method. Used when subclass wants to extend parent behavior, not completely replace it. Common in template method pattern — parent defines structure, subclass adds details.
11. Can a subclass inherit private fields from parent class?
Technically yes — private fields exist in subclass memory layout. But subclass cannot access them directly — must use inherited protected/public methods. If parent doesn't provide accessor, subclass cannot read or modify those fields.
12. What is the relationship between inheritance and cohesion?
High cohesion means a class has single, well-defined purpose. Inheritance works best when parent and child have high cohesion and strong IS-A relationship. Weak inheritance hierarchies (forced "is-a") indicate low cohesion and bad design.
13. When using inheritance with an abstract parent class, what happens if you don't implement all abstract methods?
Compilation error — concrete subclass must implement all abstract methods from parent. Unless the subclass is also declared abstract — then it can defer implementation. Abstract class can extend another abstract class and not implement its abstract methods.
14. How does inheritance affect the memory layout of objects?
Subclass object contains all fields from parent chain — parent fields live in subclass memory. Fields are laid out in inheritance order — parent fields first, then subclass fields. This is why casting to parent type doesn't lose data (upcasting is safe).
15. What is the difference between overloading and overriding in the context of inheritance?
Overloading: same method name, different parameters — resolved at compile time. Overriding: same signature as parent method — resolved at runtime via virtual dispatch. Overloading in subclasses can create confusion — ensure parameter lists are meaningfully different.
16. What is covariance in the context of method return types in inheritance?
Covariance allows overriding method to return a subtype of the parent's return type. Introduced in Java 5 — enables more specific return types without breaking contract. Example: parent method returns Animal, overriding subclass returns Dog (subtype of Animal).
17. What does it mean for a subclass to obey the Liskov Substitution Principle?
A subclass should work anywhere its parent type is expected without breaking the caller's assumptions. For example, code that sets a rectangle's width and height independently should not get surprising results when given a square.
18. When does Java insert a call to super() automatically?
The compiler inserts a no-argument super() call when a constructor does not begin with an explicit this(...) or super(...) call. This only compiles when the parent has an accessible no-argument constructor; otherwise, the child must call a matching parent constructor explicitly.
19. Why should an overriding method use @Override?
The annotation asks the compiler to confirm that the method really overrides an inherited method. If a signature is misspelled or does not match, compilation fails instead of silently creating a different overload.
20. When is composition a better choice than inheritance?
Use composition when the relationship is “has-a,” or when behavior needs to vary independently of a class hierarchy. A stack should contain a list rather than extend ArrayList, since callers should not be able to use every list operation on the stack.

Further Reading

Conclusion

Inheritance models “is-a” relationships — a Dog is an Animal, so Dog can inherit from Animal. The child class automatically receives the parent’s fields and methods, gaining reusable behavior without manual implementation. The extends keyword establishes this relationship, and super() calls ensure parent initialization runs before child-specific setup.

The inheritance hierarchy diagram shows how subclasses form a tree rooted at a common ancestor. At the top of every Java class hierarchy sits Object, which provides fundamental methods like toString(), equals(), and hashCode() (detailed in The Object Class in Java). Understanding this chain helps explain why all objects share certain behaviors.

Method overriding lets subclasses replace parent implementations with their own. The @Override annotation is not optional — it is the compiler’s sanity check that you are actually overriding something, catching typos in method names before they become runtime surprises. The @Override annotation combined with the Liskov Substitution Principle ensures subclasses honor their parent’s contract: anywhere an Animal is expected, a Dog must work.

The trade-off table reveals why composition (detailed in Composition over Inheritance) is often preferred. Inheritance creates tight coupling — the subclass is bound to the parent’s implementation details, which can change unexpectedly. When inheritance truly models an “is-a” relationship with a stable parent class, it is appropriate; but for flexible designs that need runtime behavior swapping, composition is the better tool.

Inheritance interacts with polymorphism (explored in Polymorphism in Java) — subclass instances can be stored in parent-type variables, and the correct method gets called based on the actual object type at runtime, not the reference type. This is the mechanism that makes interfaces and inheritance hierarchies powerful for writing flexible, extensible code.

Summary

Use inheritance when a stable “is-a” relationship supports substitutability and shared behavior. Keep parent contracts clear, use @Override to catch mistakes, and prefer composition when behavior needs to vary independently or inheritance would create tight coupling.

Category

Related Posts

Abstract Classes in Java

Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.

#java-abstract-classes #java #java-fundamentals

Arithmetic Operators in Java

Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.

#java-arithmetic-operators #java #java-fundamentals

Array Basics in Java

Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.

#java-array-basics #java #java-fundamentals