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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
