Inheritance in Java

Master Java inheritance: extends keyword, super, method overriding, and the substitutability principle.

published: reading time: 15 min read author: Geek Workbench

Inheritance in Java

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.

Introduction

Inheritance is the mechanism that lets subclasses automatically receive the fields and methods of their parent classes — code reuse through an “is-a” relationship. A Dog is an Animal, so Dog inherits from Animal and automatically gets name, age, eat(), and sleep() without reimplementing them. The extends keyword establishes this relationship, and Java enforces that any subclass constructor must call super() to ensure the parent is initialized before the child adds its own state.

The power of inheritance is also its trap. Inheritance creates tight coupling — the subclass is bound to the parent’s implementation details, not just its public contract. A parent that changes internally can silently break a subclass that depended on those internals. The Liskov Substitution Principle states that a subclass must work everywhere its parent works, but violating this is easier than it sounds: a Square that extends Rectangle and independently enforces equal width and height cannot be substituted for a Rectangle in code that sets width and height independently. This is the fragile base class problem.

This post covers when inheritance genuinely fits (true “is-a” with shared identity), when composition is the better tool (“has-a” relationships), the mechanics of super() calls and method overriding, and the design rules that keep inheritance hierarchies from becoming liability.

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. Overriding Non-Virtual Methods

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

Trade-off Table

Inheritance TypeCode ReuseFlexibilityCoupling
Class inheritance (extends)FullModerateTight — subclass depends on parent
Interface implementationNone (just contracts)HighLow — depends only on contract
CompositionDelegatedHighLow — depends on collaborator interface
Default method (interface)PartialModerateModerate

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
    }
}

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;
    }
}

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); }
}

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

Interview Questions

1. What is the difference between `this` and `super`?

Model Answer: "`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?

Model Answer: "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 Liskov Substitution Principle?

Model Answer: "It states that objects of a subclass should be usable wherever objects of the parent class are expected, without breaking the program. If a subclass overrides a method with different behavior that violates expectations, it breaks this principle."

4. When should you favor composition over inheritance?

Model Answer: "Favor composition when there is no true "is-a" relationship, when you need flexibility to change behavior at runtime, when inheritance would create tight coupling, or when you need to combine multiple unrelated behaviors. Composition (has-a) is more flexible than inheritance (is-a)."

5. What happens if you don't call `super()` in a subclass constructor?

Model Answer: "If the parent class has a no-arg constructor, Java implicitly calls `super()` as the first statement. However, if the parent only has parameterized constructors, you must explicitly call `super(params)`, otherwise compilation fails."

6. What is the difference between method hiding and method overriding?

Model Answer: "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."

7. Why does Java not support multiple class inheritance?

Model Answer: "Multiple class inheritance creates the diamond problem — ambiguity in which parent to use. Java designers chose single inheritance to keep type systems simple and predictable. Multiple contracts are supported via interfaces (which can extend multiple interfaces)."

8. What is the fragile base class problem in inheritance hierarchies?

Model Answer: "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."

9. What access modifier allows inheritance but prevents external access?

Model Answer: "`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."

10. What is the difference between a IS-A and HAS-A relationship?

Model Answer: "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."

11. Can you prevent a class from being subclassed?

Model Answer: "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."

12. What is method resolution order in a class hierarchy?

Model Answer: "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."

13. What happens when a subclass overrides a parent method with stricter visibility?

Model Answer: "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."

14. What is the purpose of the `super` keyword in method overriding?

Model Answer: "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."

15. Can a subclass inherit private fields from parent class?

Model Answer: "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."

16. What is the relationship between inheritance and cohesion?

Model Answer: "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."

17. When using inheritance with an abstract parent class, what happens if you don't implement all abstract methods?

Model Answer: "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."

18. How does inheritance affect the memory layout of objects?

Model Answer: "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)."

19. What is the difference between overloading and overriding in the context of inheritance?

Model Answer: "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."

20. What is covariance in the context of method return types in inheritance?

Model Answer: "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)."

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.

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