Intermediate Reading time: ~11 min

Inheritance and Interfaces

Extends vs implements, default methods, sealed classes and abstract classes

Inheritance and Composition

Inheritance and composition are the two fundamental mechanisms for building relationships between classes in Java. Choosing correctly between them is crucial for maintainable, flexible software architecture.

1. Definition

What is it?

  • Inheritance: An "is-a" relationship where a class (subclass) inherits all public and protected members of another class (superclass) using the extends keyword.
  • Composition: A "has-a" relationship where a class contains an instance of another class as a field and delegates work to it.

Why do both exist?

  • Inheritance enables code reuse and polymorphism.
  • Composition provides loose coupling and greater flexibility.
  • Together they allow developers to choose the right abstraction level for each problem.

Where does it fit?

These are two pillars of the OOP paradigm β€” most design patterns (Strategy, Decorator, Template Method, etc.) are built on top of them.


2. Core Concepts

2.1 The `extends` keyword

In Java, a class can extend exactly one other class (single inheritance):

public class Animal {
    protected String name;

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

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

2.2 The `super` keyword

super serves two main purposes:

Usage Syntax Description
Constructor call super(args) Invokes the superclass constructor β€” must be the first statement
Method call super.method() Invokes the superclass method (for overridden methods)
Field access super.field Accesses the superclass field (when hidden by the subclass)
public class Dog extends Animal {
    public Dog(String name) {
        super();          // calls Animal() constructor
        this.name = name;
    }

    @Override
    public void eat() {
        super.eat();      // calls Animal.eat()
        System.out.println("...and wants more!");
    }
}

2.3 Method overriding and `@Override`

Rules of overriding:

Rule Description
Signature Exactly matching method name and parameters
Return type Same or covariant (subclass) return type
Access modifier Same or wider (e.g., protected β†’ public)
Exception Cannot throw a broader checked exception
final method Cannot be overridden
static method Not overriding β€” this is called hiding

⚠️ The @Override annotation is not mandatory, but strongly recommended β€” it produces a compile-time error if the method doesn't actually override anything.

2.4 Constructor chaining

Constructor invocation order always proceeds top-down through the inheritance hierarchy:

public class Animal {
    public Animal() { System.out.println("Animal()"); }
}

public class Dog extends Animal {
    public Dog() { System.out.println("Dog()"); }         // ← implicit super()
}

public class Puppy extends Dog {
    public Puppy() { System.out.println("Puppy()"); }     // ← implicit super()
}

// Output of new Puppy():
// Animal()
// Dog()
// Puppy()

2.5 `final` classes and methods

Modifier Effect
final class The class cannot be extended (e.g., String, Integer)
final method The method cannot be overridden in subclasses

2.6 Composition (has-a) and delegation

Composition is not a language-level keyword β€” it is a design pattern where a class holds an instance of another class:

public class Engine {
    public void start() { System.out.println("Engine started"); }
}

public class Car {
    private final Engine engine;  // has-a relationship

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();           // delegation
    }
}

2.7 Diamond problem β€” why no multiple inheritance?

Java does not support multiple class inheritance:

      Animal
      /    \
   Flyer  Swimmer
      \    /
     FlyingFish  ← NOT POSSIBLE via extends Flyer, Swimmer

Solution: interfaces (implements) and Java 8+ default methods.

public interface Flyer { default void move() { System.out.println("Fly"); } }
public interface Swimmer { default void move() { System.out.println("Swim"); } }

public class FlyingFish implements Flyer, Swimmer {
    @Override
    public void move() {
        Flyer.super.move();  // explicit disambiguation
    }
}

2.8 Liskov Substitution Principle (LSP)

"If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program."

Simply put: a subclass must work correctly everywhere the superclass is used.


3. Practical Usage

"Favor composition over inheritance" (GoF)

This is one of the most important pieces of advice from the Gang of Four Design Patterns book.

Aspect Inheritance Composition
Relationship type is-a has-a
Coupling Tight Loose
Flexibility Decided at compile-time Swappable at runtime
Reuse direction Vertical (hierarchy) Horizontal (delegation)
Encapsulation Weakens it (subclass knows parent internals) Strengthens it

When is inheritance appropriate?

  1. True is-a relationship β€” Dog truly is an Animal
  2. Frameworks β€” extending HttpServlet, AbstractController
  3. Template Method pattern β€” abstract class defines the skeleton
  4. Sealed hierarchies (Java 17+) β€” closed domain models

When is composition the better choice?

  1. Behavior needs to change at runtime (Strategy pattern)
  2. Combining multiple distinct behaviors
  3. The "is-a" relationship isn't natural
  4. The parent class is not under your control

4. Code Examples

4.1 Basic β€” Classic inheritance with override

public abstract class Shape {
    public abstract double area();

    @Override
    public String toString() {
        return getClass().getSimpleName() + " [area=" + area() + "]";
    }
}

public class Circle extends Shape {
    private final double radius;

    public Circle(double radius) { this.radius = radius; }

    @Override
    public double area() { return Math.PI * radius * radius; }
}

public class Rectangle extends Shape {
    private final double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() { return width * height; }
}

4.2 Advanced β€” Composition replacing inheritance

Problem: InstrumentedHashSet extends HashSet, but addAll() internally calls add() β†’ double counting (Effective Java Item 18).

// ❌ BAD β€” fragile base class problem
public class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount = 0;

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);  // ← this calls this.add()! β†’ double counting
    }
}
// βœ… GOOD β€” composition + delegation (forwarding wrapper)
public class InstrumentedSet<E> implements Set<E> {
    private final Set<E> delegate;      // composition
    private int addCount = 0;

    public InstrumentedSet(Set<E> delegate) {
        this.delegate = delegate;
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return delegate.add(e);         // delegation
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return delegate.addAll(c);      // no double counting!
    }

    // ... remaining Set methods delegated ...
    @Override public int size() { return delegate.size(); }
    @Override public boolean isEmpty() { return delegate.isEmpty(); }
    // etc.
}

4.3 Decorator pattern teaser β€” composition in practice

public interface Logger {
    void log(String message);
}

public class ConsoleLogger implements Logger {
    @Override
    public void log(String message) { System.out.println(message); }
}

public class TimestampLogger implements Logger {
    private final Logger delegate;  // composition

    public TimestampLogger(Logger delegate) {
        this.delegate = delegate;
    }

    @Override
    public void log(String message) {
        delegate.log("[" + Instant.now() + "] " + message);  // decoration
    }
}

// Usage β€” combinable at runtime:
Logger logger = new TimestampLogger(new ConsoleLogger());
logger.log("Hello!");  // β†’ [2026-04-04T10:00:00Z] Hello!

5. Trade-offs

Inheritance pros and cons

βœ… Pros ❌ Cons
Natural hierarchy expression Tight coupling between super- and subclass
Polymorphism available natively Fragile base class problem
Less boilerplate code Single inheritance chain only
Simple framework extension Hierarchy depth can become unmanageable

Composition pros and cons

βœ… Pros ❌ Cons
Loose coupling β€” components are independent More code (delegating methods)
Runtime swappability No native polymorphism (interface needed)
Multiple behaviors combinable Sometimes more complex structure
Better testability (mockable)

6. Common Mistakes

❌ 1. Breaking LSP

// The Rectangle and Square problem
public class Rectangle {
    protected int width, height;
    public void setWidth(int w) { this.width = w; }
    public void setHeight(int h) { this.height = h; }
    public int area() { return width * height; }
}

public class Square extends Rectangle {
    @Override public void setWidth(int w) { this.width = this.height = w; }   // ← LSP violation!
    @Override public void setHeight(int h) { this.width = this.height = h; }
}

// Client code breaks:
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(3);
assert r.area() == 15;  // FAIL! area() == 9

❌ 2. Excessively deep inheritance hierarchies

Example of an overgrown hierarchy: Object β†’ BaseEntity β†’ AuditableEntity β†’ SoftDeletableEntity β†’ User β†’ AdminUser

πŸ”΄ A hierarchy 5+ levels deep becomes unmaintainable. Max 2-3 levels is recommended.

❌ 3. Calling overridable methods from constructors

public class Parent {
    public Parent() {
        init();  // ← DANGEROUS! Child's init() is called, but child fields aren't initialized yet
    }
    protected void init() { /* ... */ }
}

public class Child extends Parent {
    private final String value = "hello";

    @Override
    protected void init() {
        System.out.println(value.length());  // NullPointerException! value is still null
    }
}

❌ 4. Omitting the `@Override` annotation

// Typo without @Override β†’ not an override, it's a new method!
public class MyList extends ArrayList<String> {
    public boolean Add(String s) { ... }  // ← capital 'A' β€” not an override!
}

7. Senior-level Insights

7.1 Effective Java Item 18

"Favor composition over inheritance" β€” Josh Bloch

Inheritance breaks encapsulation because the subclass depends on the superclass's implementation details (not just its API). If the superclass's internal behavior changes, the subclass can break.

7.2 Template Method vs Strategy pattern

Aspect Template Method Strategy
Mechanism Inheritance β€” abstract method Composition β€” interface reference
When decided? Compile-time Runtime
Flexibility One behavior variation Multiple, swappable behaviors
Example AbstractList.get() Passing Comparator to sort()

7.3 Sealed hierarchies for domain modeling (Java 17+)

sealed classes and interfaces restrict the set of permitted subclasses:

public sealed interface PaymentMethod
    permits CreditCard, BankTransfer, DigitalWallet {
}

public record CreditCard(String number, String expiry) implements PaymentMethod {}
public record BankTransfer(String iban) implements PaymentMethod {}
public record DigitalWallet(String provider) implements PaymentMethod {}

Benefits:

  • The compiler performs exhaustive checks in switch expressions
  • Safe domain model β€” nobody can add new implementations
  • Combined with pattern matching, produces very expressive code
String describe(PaymentMethod pm) {
    return switch (pm) {
        case CreditCard cc    -> "Card ending in " + cc.number().substring(12);
        case BankTransfer bt  -> "IBAN: " + bt.iban();
        case DigitalWallet dw -> "Wallet: " + dw.provider();
    };
}

8. Glossary

Term Definition
Inheritance A class inherits members of another class (extends)
Composition A class holds an instance of another class as a field (has-a)
Delegation Forwarding a task to the contained object
Method overriding Replacing the superclass's method implementation in a subclass
Constructor chaining The chain of constructor invocations through the inheritance hierarchy
Fragile base class When internal changes in the superclass break the subclass
LSP Liskov Substitution Principle β€” subclass must be substitutable for the superclass
Diamond problem Ambiguity caused by multiple inheritance
Sealed class A class with a restricted set of permitted subclasses (Java 17+)
Forwarding Delegation through a wrapper class

9. Cheatsheet

Aspect Inheritance (is-a) Composition (has-a)
Example class Dog extends Animal class Car { private Engine engine; }
Benefit Natural hierarchy Loose coupling
Benefit Polymorphism Runtime swappability
Trade-off Tight coupling More boilerplate
Trade-off Single inheritance No inherited base behavior by default
  • Always add @Override for compile-time safety.
  • super() must be the first statement in a constructor.
  • final classes and methods cannot be extended or overridden.
  • Keep hierarchies to 2-3 levels whenever possible.
  • LSP means the subclass must remain substitutable for the superclass.
  • Default choice: prefer composition unless inheritance is clearly natural.