Fundamentals 11 min read

When to Use Composition, Inheritance, or Delegation in Java? A Practical Guide

This article explains how Java developers can reuse code without duplication by employing composition, inheritance, and delegation, details the syntax and initialization rules for each, and clarifies the use of the final keyword for data, methods, parameters, and classes.

Java One
Java One
Java One
When to Use Composition, Inheritance, or Delegation in Java? A Practical Guide

Reusing Classes without Copying Code

Reusing classes means creating new classes that leverage existing code without copying it. The article introduces three mechanisms: composition, inheritance, and a hybrid approach called delegation.

1. Composition Syntax

class WaterSource {
    private String s;
    WaterSource() {
        System.out.println("WaterSource()");
        s = "Constructed";
    }
}

private class SprinklerSystem {
    private String value;
    private WaterSource source = new WaterSource();
}

2. Inheritance Syntax

2.1 Syntax

class Cleaner {
    private String s = "Cleaner";
    public void append(String a) { s += a; }
    public void scrub() { append("scrub()"); }
}

public class Detergent extends Cleaner {
    public void scrub() {
        append("Detergent.scrub()");
        super.scrub();
    }
}

Base‑class methods are usually private; exported methods are public.

To use a base‑class method from the subclass, declare it public.

If method names clash, the subclass can invoke the base version with super.

2.2 Initializing the Base Class

When a subclass object is created, it contains a sub‑object of the base class. The base sub‑object is created inside the subclass, not externally.

Initialization order:

The subclass calls the base constructor to create the base sub‑object.

The subclass constructor runs to finish its own initialization.

If a default constructor is used, the compiler inserts these steps automatically; with parameterized constructors, super(...) must be called explicitly.

2.3 Cleanup

Java relies on garbage collection, but explicit cleanup is often placed in a finally block. The cleanup order should be:

Clean up subclass fields in reverse construction order.

Invoke the base class’s cleanup method.

2.4 Name Hiding

When a subclass defines a method with the same signature as a base method, use @Override to make the intention clear and avoid accidental hiding.

class Lisa extends Homer {
    @Override
    void doh(Milhouse m) {
        System.out.println("doh(Milhouse m)");
    }
}

If the base class also defines doh(Milhouse m), the compiler will flag an error, preventing accidental overrides.

2.5 Upcasting

Converting a subclass reference to a base‑class reference is called upcasting. It is safe and reflects the traditional inheritance diagram where the arrow points upward.

If you need upcasting, inheritance is the appropriate mechanism; otherwise, prefer composition.

3. Delegation Syntax

Delegation sits between composition and inheritance. It places a member object inside the new class (like composition) while exposing the member’s methods through the new class (like inheritance).

public class SpaceShipControls {
    void up(int velocity) {}
    void down(int velocity) {}
}

public class SpaceShipDelegation {
    private String name;
    private SpaceShipControls controls = new SpaceShipControls();

    public SpaceShipDelegation(String name) { this.name = name; }

    public void up(int velocity) { controls.up(velocity); }
    // down method can be omitted to hide it
}

This approach provides the same interface as inheritance but allows selective exposure of methods.

4. The final Keyword

4.1 Final Data

When applied to a field:

For primitive types, it creates an immutable compile‑time constant or a runtime constant that cannot be changed.

For object references, the reference cannot be reassigned, but the object’s internal state can still change.

class Value { int i; public Value(int i){ this.i = i; } }

public class FinalData {
    private final int valueOne = 1;               // compile‑time constant
    private static final int VALUE_TWO = 2;       // static constant
    public static final int VALUE_THREE = 3;      // static constant
    private final Value v4 = new Value(4);
    private static final Value VAL_5 = new Value(5);
    private final int[] a = {1,2,3};

    public static void main(String[] args) {
        FinalData fd1 = new FinalData();
        for (int i = 0; i < fd1.a.length; i++) {
            fd1.a[i]++; // allowed: modifying array contents
        }
    }
}

4.2 Blank final

A blank final field is declared final without an initializer and must be assigned exactly once before the constructor finishes.

class BlankFinal {
    private final int i = 0;
    private final int j; // blank final
    public BlankFinal() { j = 1; }
}

4.3 Final Parameters

Parameters declared final cannot be reassigned inside the method.

public class FinalArguments {
    private Integer age;
    public Integer add(final Integer year) {
        return year + age; // year cannot be modified
    }
}

4.4 Final Methods

A final method cannot be overridden by subclasses. Private methods are implicitly final because they are not visible to subclasses.

4.5 Final Classes

A class declared final cannot be subclassed. All its methods are effectively final, and the class can still contain final or non‑final fields as desired.

5. Inheritance Initialization Order

Memory for the object is zero‑filled.

The base‑class constructor runs.

Instance fields are initialized in the order they are declared.

The subclass constructor body executes.

JavaOOPInheritancecompositionfinalDelegation
Java One
Written by

Java One

Sharing common backend development knowledge.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.