Inheritance vs Composition in Java

Inheritance vs Composition in Java

Object reuse looks simple: extend a class and gain behavior. In production systems, inheritance creates long term risk unless design supports extension intentionally.

Favor Composition Over Inheritance

Inheritance creates an is-a relationship. Composition creates a has-a relationship.

Inheritance couples behavior to internal implementation. Composition delegates behavior through a field.

Problem pattern:
class RiskList extends ArrayList<Trade> {
    @Override
    public boolean add(Trade t) {
        log(t);
        return super.add(t);
    }
}        

ArrayList contains many internal methods calling add. Your override triggers multiple times, sometimes unexpectedly.

Result: duplicated logging, broken metrics, unstable behavior after JDK upgrades.

Composition pattern:
class RiskList {
    private final List<Trade> trades = new ArrayList<>();

    public boolean add(Trade t) {
        log(t);
        return trades.add(t);
    }
}        

Now behavior stays under control. No surprise calls. No dependency on internal structure.

Example

Order management systems track trades across pricing, margin, and settlement. Extending HashMap to inject audit logic breaks once JDK changes internal call paths. Using composition around Map preserves audit accuracy across releases.

Design for Inheritance or Prohibit It

Inheritance only works when a class is built for extension.

If subclassing stays allowed, documentation must explain:

  • Which methods call other methods
  • Which hooks subclasses override safely
  • What invariants subclasses must preserve

Example of dangerous design:
class BaseRiskEngine {
    public BaseRiskEngine() {
        init();
    }

    protected void init() { }
}        
Subclass:
class EquityRiskEngine extends BaseRiskEngine {
    private MarketData data;

    public EquityRiskEngine() {
        data = load();
    }

    @Override
    protected void init() {
        data.validate();
    }
}        

Construction order:

  1. Base constructor runs
  2. init() executes
  3. data stays null
  4. NullPointerException occurs

The subclass method runs before subclass state exists.

This failure hides inside constructors.

Constructor Rule

Constructors must never call overridable methods.

If extension was not part of design, block it.

public final class PricingEngine {
}        

Final communicates intent and protects behavior.

Example

Risk engines run during market open under tight latency budgets. Subclass constructors calling overridden methods produce partial initialization. Pricing fails before desks receive quotes.

Final classes prevent accidental extension across teams.

Design Rules

  • Prefer delegation over inheritance
  • Expose extension only with documentation
  • Never call overridable methods from constructors
  • Mark classes final when extension was not planned


Takeaway

Inheritance multiplies coupling. Composition contains behavior.

To view or add a comment, sign in

More articles by Ankur Mistry

  • Lambdas, Method References, Streams

    Prefer Lambdas to Anonymous Classes Before Java 8: With lambdas: Shorter. Clearer.

  • Generics in Java

    Generics replace runtime failure with compile time guarantees. Before Java 5, casts hid defects until production…

  • Prefer Interfaces to Abstract Classes

    Java supports single inheritance. Extending an abstract class consumes the only superclass slot.

  • Control Access to Protect Design

    Encapsulation drives decoupling. Once access spreads, refactoring cost rises across teams and systems.

  • Methods Common to All Objects

    Obey the General Contract When Overriding equals Overriding equals looks simple: check whether two objects match. In…

  • Avoid Finalizers and Cleaners

    Never rely on object finalization for resource management. In C++, destructors run at deterministic points.

  • Avoid Creating Unnecessary Objects

    This item sounds simple, yet performance loss often starts here. Classic Mistake: Redundant Allocation Better form:…

  • Prefer Dependency Injection to Hardwiring Resources

    A design rule many engineers follow instinctively, yet often violate under time pressure. Problem: Hardwired…

  • Enforce Noninstantiability

    Some classes exist only as containers for static methods and constants. Examples from the JDK: These classes represent…

  • Singleton Instance

    Enforcing a Single Instance with the Singleton Property After covering factories and builders, object creation control…

Explore content categories