📐 Section 18 · Question #11

When is it better to use composition instead of inheritance

In modern software architecture, the foundational rule established by Joshua Bloch in Effective Java states:


🟢 Junior Level

In modern software architecture, the foundational rule established by Joshua Bloch in Effective Java states:

“Favor composition over inheritance.”

Inheritance creates the tightest possible coupling in Object-Oriented Programming. The subclass becomes permanently tied to all bugs, implementation details, security vulnerabilities, and future modifications of its parent class. Composition keeps classes independent, loosely coupled, and modular.

Hand Tool vs. Bone Fusion Analogy:
+-------------------------------------------------------------------------+
| • Inheritance (Bone Fusion): Permanently fusing an axe to your arm.     |
|   You can chop wood, but sleeping, driving, or washing dishes becomes   |
|   nearly impossible.                                                    |
| • Composition (Hand Tool): Holding the axe in your hand. You chop wood, |
|   put the axe on the rack, and pick up a coffee cup.                    |
+-------------------------------------------------------------------------+

Top 3 Signals When Composition is Mandatory

  1. No Genuine “IS-A” Relationship:
    • A Car needs an Engine, but a car is not an engine (Car extends Engine is an architectural error). A car has an engine (Car composes Engine).
  2. The Base Class Was Not Designed for Extension:
    • Extending concrete standard classes (ArrayList, HashSet, HashMap) frequently introduces hidden invariant violations and fragility.
  3. You Need to Restrict the Public API:
    • If you build a Queue (FIFO structure), inheriting from ArrayList accidentally exposes list.remove(int index) and list.clear(), allowing callers to violate the queue’s ordering semantics.

🟡 Middle Level

5 Architectural Scenarios Where Composition Wins

1. Protecting Against API Pollution

The classic historical defect in the JDK is java.util.Properties:

// JDK Design Flaw: Properties extends Hashtable<Object, Object>
Properties props = new Properties();
props.setProperty("timeout", "5000"); // Standard String key/value API

// Inherited Hashtable methods allow corrupting internal String guarantees:
props.put(new Object(), 12345); 
props.getProperty("timeout"); // Risks throwing ClassCastException at runtime!

Rule: If a subclass should not expose 100% of its parent’s public methods, inheritance is forbidden. Wrap the object and expose only targeted methods.


2. Dynamic Runtime Strategy Swapping

Inheritance is locked statically at compile time. Composition allows changing algorithms dynamically during execution:

public class OrderService {
    private TaxStrategy taxStrategy;

    public void setTaxStrategy(TaxStrategy taxStrategy) {
        this.taxStrategy = taxStrategy; // Swapped dynamically at runtime!
    }

    public BigDecimal checkout(Order order) {
        return taxStrategy.calculateTax(order);
    }
}

3. Preventing Combinatorial Class Explosion

Imagine an alert system requiring:

  • Channels (3): Email, SMS, Telegram.
  • Formats (3): PlainText, HTML, Markdown.
  • Security (2): Plain, PGP-Encrypted.
• Inheritance Approach: 3 * 3 * 2 = 18 rigid classes
  (EncryptedMarkdownTelegramAlert, PlainHtmlEmailAlert, etc.)

• Composition Approach (Decorator / Bridge): 3 + 3 + 2 = 8 modular classes
  composed dynamically: new EncryptedAlert(new MarkdownAlert(new TelegramSender()))

4. Decoupling Concurrency Locks

  • With inheritance, marking an overridden method as synchronized locks the entire instance monitor (this).
  • With composition, an application can manage granular, independent locks across decoupled subsystems:
    public class HighThroughputService {
      private final Object writeLock = new Object();
      private final MetricsRegistry metrics; // Composed delegate
    
      public void processData() {
          synchronized (writeLock) {
              // State mutation under local lock
          }
          metrics.recordExecution(); // Internal independent locking inside metrics!
      }
    }
    

5. Isolated Unit Testability

A service that composes its dependencies through constructor injection (public UserService(UserRepository repo, MailClient mail)) can be tested in microseconds using Mockito:

UserService service = new UserService(mockRepo, mockMail);

Conversely, a subclass inheriting from a heavy base class (BaseDatabaseService) drags database connection pools, environmental configs, and static state into every test.


🔴 Senior Level

1. Joshua Bloch’s Rule: “Design and Document for Inheritance or Else Prohibit It”

In Effective Java (Item 19), Joshua Bloch specifies that if a class is not specifically engineered for extension:

  1. Prohibit Subclassing: Mark the class final, or make constructors package-private / private with static factory methods.
  2. Eliminate Constructor Self-Use: Constructors must never invoke overridable methods. In Java, parent constructors run before child constructors. If a parent constructor calls an overridden method, the child’s method executes before the child has initialized its final fields, resulting in silent null reads or NullPointerExceptions!
  3. Document Self-Use with @implSpec: If designing an abstract base class, explicitly document which methods call other overridable methods.

2. Machine-Level Performance Trade-Offs in HotSpot JVM

Evaluation Vector Class Inheritance Object Composition
VTable Depth & Dispatch Deep class hierarchies extend virtual method tables (vtable), causing call sites with multiple subclasses to become megamorphic. Uses invokeinterface / invokevirtual. When the delegate is monomorphic, HotSpot C2 inlines it completely into direct machine instructions.
Memory Footprint & Padding Fields of parent and child are packed into one contiguous heap block. Single 12–16 byte object header. Multiple heap objects. Each composed object incurs a 16-byte header + 4–8 byte reference pointer + 8-byte alignment padding.
CPU Cache Locality Superior spatial locality. Continuous memory layout fits neatly into CPU L1/L2 cache lines. Pointer Chasing: Dereferencing references to separate heap addresses can trigger CPU cache misses if objects are scattered.

3. Architectural Decision Framework

Need to organize a relationship between Class A and Class B?
                     |
         Is there a strict, pure IS-A relationship?
         Can B substitute A in 100% of cases without breaking invariants?
                    / \
             [NO]  /   \  [YES]
                  /     \
    +-------------+      Is the superclass explicitly designed
    | COMPOSITION |      and documented for extension?
    +-------------+     (Template Method, Exceptions, @MappedSuperclass)
                               / \
                        [NO]  /   \  [YES]
                             /     \
               +-------------+     +-------------+
               | COMPOSITION |     | INHERITANCE |
               +-------------+     +-------------+

4 Tricky Questions

1. Why is java.util.Stack in the JDK considered a severe architectural defect and how should it have been built?

Answer: Stack was implemented by extending Vector (class Stack<E> extends Vector<E>). This violated encapsulation and the IS-A relationship:

  • A stack is strictly a LIFO structure with push(), pop(), and peek().
  • By extending Vector, it inherited indexed random-access methods like get(int index) and insertElementAt(element, 0). Callers can insert or remove elements from the bottom or middle of the stack, corrupting LIFO invariants.

Proper Design: Use composition. Stack should hold an internal Deque<E> (such as ArrayDeque) as a private field and expose only four methods: push(), pop(), peek(), and isEmpty().

2. Can composition ever degrade performance compared to inheritance, and when?

Answer: Yes, in specialized High-Frequency Trading (HFT), real-time graphics, and game engine development. With composition, accessing fields in a delegate requires pointer dereferencing (Pointer Chasing). If composed objects reside in disparate memory addresses across the garbage-collected heap, traversing references triggers CPU L1/L2 cache misses (incurring a 100–200 cycle RAM latency penalty).

With inheritance, the JVM allocates all parent and child fields contiguously in a single heap block, guaranteeing optimal CPU spatial cache line locality.

3. How does the Delegating Wrapper (Forwarding Class) pattern solve the Fragile Base Class problem?

Answer: To avoid writing boilerplate delegation methods for large interfaces, create an abstract ForwardingSet<E> implements Set<E> that forwards every call to an underlying Set<E> instance. A custom class (InstrumentedSet<E>) extends ForwardingSet<E> and overrides only the target methods (e.g., add()).

Unlike direct inheritance from HashSet, invoking addAll() forwards directly to delegate.addAll(), running the delegate’s internal logic safely without triggering double-counting recursion bugs.

4. What happens during Java serialization if a composed delegate does not implement Serializable?

Answer: Attempting to serialize the outer object via ObjectOutputStream.writeObject(this) throws a runtime java.io.NotSerializableException. Remediation:

  1. Declare the delegate field as private transient Delegate delegate;.
  2. Implement custom serialization lifecycle hooks: writeObject(ObjectOutputStream out) and readObject(ObjectInputStream in).
  3. Manually write the delegate’s primitive state to the stream, and reconstruct the delegate instance during deserialization.

🎯 Interview Cheat Sheet

Core Concepts:

  • Core Principle: “Favor composition over inheritance” (Effective Java Item 18).
  • Key Scenarios for Composition:
    1. No pure IS-A relationship exists.
    2. The target class was not explicitly designed for inheritance.
    3. You need to restrict or sanitize the public API (avoiding API pollution like Properties).
    4. Behavior must change dynamically at runtime (Strategy / State patterns).
    5. Protection against the Fragile Base Class problem (superclasses changing internal method interactions).
  • When Inheritance is Justified: Framework Template Methods, custom Exception hierarchies, and JPA @MappedSuperclass.

Red Flags to Avoid:

  • ❌ Subclassing concrete JDK collections (ArrayList, HashMap) to create business data structures.
  • ❌ Using inheritance solely to avoid typing boilerplate delegation methods.
  • ❌ Calling overridable methods inside class constructors.