📐 Section 18 · Question #10

What is composition and inheritance

Inheritance allows a new class to be derived from an existing class. The subclass automatically inherits the fields and methods of its parent:


🟢 Junior Level

Inheritance and Composition are the two fundamental mechanisms in Object-Oriented Programming (OOP) for structuring relationships between classes and sharing code:

Inheritance (The “IS-A” Relationship)

Inheritance allows a new class to be derived from an existing class. The subclass automatically inherits the fields and methods of its parent:

  • Expresses an “IS-A” relationship (“A Dog is an Animal”).
  • Declared in Java using the extends keyword.
public class Animal {
    public void eat() {
        System.out.println("Animal is eating");
    }
}

public class Dog extends Animal { // Dog IS-A Animal
    public void bark() {
        System.out.println("Woof!");
    }
}

Composition (The “HAS-A” Relationship)

Composition is an approach where a class holds a reference to an instance of another class as a private field and delegates work to it:

  • Expresses a “HAS-A” relationship (“A Car has an Engine”).
  • The lifecycle of the contained object is typically bound to the lifecycle of the container.
public class Engine {
    public void start() {
        System.out.println("Engine started");
    }
}

public class Car {
    // Car HAS-A Engine (Composition)
    private final Engine engine;

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

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

Rule of Thumb for Beginners

  • If there is a strict behavioral taxonomy where a subtype can transparently replace the parent everywhere without surprises, use inheritance.
  • If an entity is assembled from modular components or needs helper functionality, use composition.

🟡 Middle Level

1. Composition vs. Aggregation

In UML and object modeling, containment is categorized into two distinct ownership models:

  1. Composition (Strong Ownership):
    • The child cannot exist without the parent. When the parent is garbage-collected or deleted, its parts are destroyed as well.
    • Example: An Order and its OrderItem lines. An order line has no domain identity or purpose without its parent order.
  2. Aggregation (Loose Ownership):
    • The child has an independent lifecycle. The parent holds a reference, but does not own its existence.
    • Example: A Department and its Employee members. If the department is closed, the employees remain with the company.

2. White-Box Reuse vs. Black-Box Reuse

Metric Inheritance (White-Box Reuse) Composition (Black-Box Reuse)
Visibility “White box”: Subclass sees internal protected fields and methods of the superclass. “Black box”: Container interacts exclusively via the public contract (API) of the component.
Binding Time Static (Compile-time): The relationship is fixed at compile time and cannot change during execution. Dynamic (Runtime): The implementation can be swapped dynamically at runtime (Strategy / State pattern).
Encapsulation Violated: Subclasses depend on hidden implementation details of the parent. Preserved: Component internals remain strictly private and encapsulated.
Testability Harder: Mocking parent methods without side effects requires complex subclassing or heavy frameworks. Trivial: Dependencies are injected via interfaces, allowing pure unit test mocks.

3. The Fragile Base Class Problem

In Effective Java (Item 18), Joshua Bloch established the principle: “Favor composition over inheritance.”

The primary hazard of inheritance is the Fragile Base Class Problem, where modifying the internal implementation of a parent class silently breaks its subclasses without compilation errors:

// ❌ INHERITANCE TRAP: Attempting to count added items
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); // 💥 BUG: HashSet.addAll() internally invokes add()!
    }

    public int getAddCount() { return addCount; }
}
InstrumentedHashSet<String> set = new InstrumentedHashSet<>();
set.addAll(List.of("A", "B", "C"));
System.out.println(set.getAddCount()); // Prints 6 instead of 3!

Why it prints 6: HashSet.addAll() iterates over the collection and calls this.add(). Because add() was overridden in the subclass, addCount was incremented once by 3 in addAll(), and then 3 additional times by 1 in add(). The subclass was compromised by an internal, undocumented implementation detail of the JDK base class.

Refactoring via Composition and Forwarding:

// ✅ SAFE COMPOSITION: Zero coupling to HashSet internals
public class InstrumentedSet<E> implements Set<E> {
    private final Set<E> delegate;
    private int addCount = 0;

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

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

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

    public int getAddCount() { return addCount; }

    // Forward remaining Set methods directly to delegate...
    @Override public int size() { return delegate.size(); }
    @Override public boolean isEmpty() { return delegate.isEmpty(); }
    @Override public boolean contains(Object o) { return delegate.contains(o); }
}

This composition-based wrapper works with 100% predictability across any Set implementation (HashSet, TreeSet, LinkedHashSet).


🔴 Senior Level

1. Physical Object Layout in JVM Memory

In the HotSpot JVM, inheritance and composition produce fundamentally different memory structures:

Memory Layout in Inheritance:

All fields of the parent and child are packed into a single continuous memory block on the heap:

+-----------------------------------------------------------------------+
| Mark Word (8B) | Klass Word (4B/8B) | Parent Fields | Subclass Fields |
+-----------------------------------------------------------------------+
  • Advantages: Excellent CPU L1/L2 data cache locality; single 12–16 byte object header; reduced work for the garbage collector (GC tracks a single reference).
  • Disadvantages: Rigid compile-time memory footprint; impossible to swap components at runtime.

Memory Layout in Composition:

The container object stores a heap reference pointing to a completely separate object:

[Car Object (16B header + 4B/8B pointer)] ---> [Engine Object (16B header + fields)]
  • Advantages: Dynamic polymorphism (swapping GasEngine for ElectricEngine); flyweight sharing; independent thread synchronization locks.
  • Overhead: Extra object header (16 bytes); reference pointer overhead; potential pointer chasing leading to CPU cache misses during deep traversals.

2. JIT Compilation, Devirtualization, and Inlining

  • Inheritance: Deep class hierarchies (4+ levels) with overridden methods rely on virtual method tables (vtable lookups via invokevirtual). When multiple concrete subclasses execute at the same call site, the site becomes megamorphic, disabling C2 JIT inlining.
  • Composition: When a composed delegate field is declared as private final Delegate delegate, the HotSpot C2 compiler recognizes that the reference never changes after constructor completion. If the delegate class is monomorphic, the JIT compiler eliminates delegation overhead entirely through Method Inlining: the machine code instructions of the delegate method are embedded directly into the calling method without function call penalties.

4 Tricky Questions

1. In what scenarios in modern enterprise development is class inheritance clearly preferred over composition?

Answer:

  1. Template Method Pattern: When an invariant algorithmic framework and lifecycle order is fixed in an abstract base class (abstract class BaseReportGenerator), with individual hook steps overridden by subclasses.
  2. Exception Hierarchies: Custom business exceptions must inherit from Exception or RuntimeException, as the JVM exception unwinding mechanism (athrow bytecode) requires class inheritance from java.lang.Throwable.
  3. ORM / JPA Mapped Superclasses: Using @MappedSuperclass (e.g., BaseAuditableEntity containing id, createdAt, updatedAt, version) avoids duplicating database audit fields across entities without creating unnecessary join tables.

2. What is the Diamond Problem and how does Java handle it for classes and interfaces?

Answer: The Diamond Problem occurs in multiple inheritance when class D inherits from both B and C, which both inherit from A with method foo(). It is ambiguous which implementation D should inherit.

  • For Classes: Java forbids multiple class inheritance (extends OnlyOneClass), completely eliminating the diamond problem for instance fields.
  • For Interfaces with Default Methods: Java resolves conflicts via three compiler rules:
    1. Classes win: A method declared in a class or superclass takes precedence over any interface default method.
    2. Sub-interfaces win: A default method in a more specific interface overrides one in a more general parent interface.
    3. Explicit disambiguation: If two independent interfaces provide the same default signature, compilation fails until the implementer explicitly overrides the method: InterfaceA.super.foo().

3. Does composition violate encapsulation if a class provides a getter for its internal delegate?

Answer: Yes. Exposing an internal mutable delegate via a public getter causes Representation Exposure.

If Car provides public Engine getEngine(), external callers can directly modify the engine’s internal state (e.g., setting RPM to dangerous thresholds) bypassing all of Car’s domain validation rules. To maintain encapsulation, either do not provide a getter, return a Defensive Copy, or expose a read-only unmodifiable view interface.

4. Why did Sealed Classes in Java 17+ fundamentally change the architectural view on inheritance?

Answer: Historically, class inheritance was considered hazardous because library authors could not prevent third-party code from extending and corrupting class invariants.

sealed classes (public sealed class Shape permits Circle, Rectangle) allow developers to strictly declare permitted subclasses at compile time. This restores predictability:

  • The hierarchy is mathematically closed.
  • The compiler enforces exhaustive pattern matching in switch expressions without fallback default branches.
  • HotSpot JIT C2 performs more aggressive monomorphic and bimorphic devirtualization and inlining.

🎯 Interview Cheat Sheet

Core Concepts:

  • Inheritance (IS-A): Static compile-time relationship; white-box reuse; subclass depends on parent implementation details.
  • Composition (HAS-A): Dynamic runtime relationship; black-box reuse; container interacts solely through public contracts.
  • Joshua Bloch Rule: “Favor composition over inheritance” (Effective Java Item 18).
  • Fragile Base Class: Superclass changes silently invalidate subclass assumptions (e.g., InstrumentedHashSet).
  • JIT Inlining: Declaring composed delegates as private final allows C2 to inline calls with zero runtime overhead.

Red Flags to Avoid:

  • ❌ “Inheritance is always better than composition because it avoids writing boilerplate forwarding methods.” (Trades short-term keystrokes for long-term fragility).
  • ❌ “Composition is always significantly slower than inheritance.” (JIT devirtualization and inlining eliminate delegation overhead in monomorphic paths).
  • ❌ “Subclassing ArrayList is the standard way to build a domain list.” (Wrap List<T> via composition to maintain encapsulation).