☕ Java
Composition
Composition is the strongest form of the has-a relationship, where the whole object creates, owns, and is entirely responsible for the lifecycle of its parts. The parts exist only as components of the whole — they are created when the whole is created, they have no meaningful existence outside the whole, and they are destroyed when the whole is destroyed. Composition is the preferred mechanism for building complex objects from simpler ones in object-oriented design, and is widely favoured over inheritance for code reuse.
Understanding Composition — The Whole Owns Its Parts
Composition is characterised by strong ownership and lifecycle coupling. The whole object is responsible for creating its parts, and the parts exist exclusively within and for the whole. The parts have no independent identity outside the composition — they are not passed in from outside, they are not shared with other objects, and they cannot meaningfully exist once the whole is destroyed.
Consider a Car and its Engine. A car's engine is not something that pre-existed independently and was associated with the car — the engine is an integral component of that specific car. When the car is manufactured, the engine is created as part of it. When the car is scrapped, the engine goes with it. You would not typically take an engine out of one car and put it in another (in an object model — real mechanics aside). The engine is a component of the car, not a separate entity that happens to be inside it.
Similarly, a Form and its Fields form a composition. The TextBox, CheckBox, and RadioButton components that make up a Form are created when the Form is designed — they are not independent objects that exist before the Form. When the Form is disposed of, its fields are disposed with it. The fields have no purpose or existence outside their containing Form.
The Java implementation of composition uses fields that are initialised through new expressions inside the composing class's constructor or through field initialisers. The constructed parts are never exposed as references that external code could hold onto and outlive the whole. Typically, the parts are not exposed at all, or only through controlled interfaces that do not give external code a direct reference to the part object itself.
Java
// ── Composition: Engine exists only as part of Car: ──────────────────
public class Engine {
private final int horsePower;
private final String fuelType;
private boolean running = false;
// Package-private or private constructor — only Car creates Engine:
Engine(int horsePower, String fuelType) {
this.horsePower = horsePower;
this.fuelType = fuelType;
}
void start() {
running = true;
System.out.println(horsePower + "hp engine started");
}
void stop() {
running = false;
System.out.println("Engine stopped");
}
boolean isRunning() { return running; }
int getHorsePower() { return horsePower; }
}
public class Transmission {
private int currentGear = 0;
private final int maxGears;
Transmission(int maxGears) { this.maxGears = maxGears; }
void shiftUp() {
if (currentGear < maxGears) {
currentGear++;
System.out.println("Shifted to gear " + currentGear);
}
}
void shiftDown() {
if (currentGear > 0) {
currentGear--;
System.out.println("Shifted to gear " + currentGear);
}
}
int getCurrentGear() { return currentGear; }
}
public class Car {
private final String make;
private final String model;
// Composition: Car creates and OWNS Engine and Transmission:
private final Engine engine;
private final Transmission transmission;
public Car(String make, String model, int horsePower) {
this.make = make;
this.model = model;
// Parts created inside the constructor — Car owns them:
this.engine = new Engine(horsePower, "Petrol");
this.transmission = new Transmission(6);
}
public void start() {
engine.start();
System.out.println(make + " " + model + " is ready to drive");
}
public void accelerate() {
if (!engine.isRunning()) {
System.out.println("Start the car first!");
return;
}
transmission.shiftUp();
}
public void stop() {
transmission.shiftDown();
engine.stop();
}
public String getInfo() {
return make + " " + model + " (" + engine.getHorsePower() + "hp)";
}
}
Car myCar = new Car("Toyota", "Supra", 335);
myCar.start();
myCar.accelerate();
myCar.accelerate();
myCar.stop();
// When myCar goes out of scope, engine and transmission
// become unreachable and are garbage collected with it.Composition Over Inheritance
"Prefer composition over inheritance" is one of the most widely cited principles in object-oriented design, appearing in the Gang of Four Design Patterns book and endorsed by virtually every major authority on Java design. It means that when you need to reuse behaviour from another class, building a class that contains an instance of that other class (composition) is often better than extending it (inheritance).
Inheritance creates a permanent, compile-time coupling between the subclass and the superclass. Every change to the superclass potentially affects all subclasses. The subclass is exposed to all the implementation details of the superclass — a change to how a superclass computes something, even a private implementation change, can break subclass behaviour if the subclass overrides a method that depended on the old implementation. This is the fragile base class problem.
Composition creates a more flexible and controllable coupling. The composing class uses the other class through its public interface only. Internal changes to the composed class do not affect the composing class as long as the interface is stable. The composing class can also switch the composed object at runtime, something impossible with inheritance. And the composing class can use multiple composed objects, each providing different capabilities — while a class can only inherit from one superclass.
The practical guideline is to use inheritance for genuine is-a relationships where the subclass truly is a specialisation of the superclass, where the Liskov Substitution Principle is satisfied, and where the subclass relationship is stable. Use composition for has-a relationships, for code reuse without the is-a semantic, and when flexibility to change or combine behaviours at runtime is needed.
Java
// ── Inheritance approach — fragile, inflexible: ──────────────────────
// Problem: we want SortedList to be like ArrayList but sorted.
// Inheritance seems natural — SortedList IS-A ArrayList.
public class SortedList<T extends Comparable<T>> extends ArrayList<T> {
@Override
public boolean add(T element) {
super.add(element);
sort(null);
return true;
}
// BUT: this is broken! addAll() in ArrayList calls add() in a loop,
// which would work, but ArrayList has MANY methods that add elements:
// add(index, element), addAll(), set(), etc.
// We must override ALL of them to maintain the sorted invariant.
// And if ArrayList adds a new method in a future JDK version,
// our SortedList silently breaks.
// This is the fragile base class problem.
}
// ── Composition approach — flexible, resilient: ───────────────────────
public class SortedList<T extends Comparable<T>> {
// Compose with ArrayList rather than extend it:
private final List<T> inner = new ArrayList<>();
public void add(T element) {
inner.add(element);
Collections.sort(inner);
}
public void addAll(Collection<T> elements) {
inner.addAll(elements);
Collections.sort(inner);
}
public T get(int index) { return inner.get(index); }
public int size() { return inner.size(); }
public boolean contains(T e) { return inner.contains(e);}
// We expose ONLY what makes sense for a sorted list.
// ArrayList's set(index, value) is intentionally not exposed —
// it would break the sorted invariant.
// Future ArrayList changes do not affect our class.
}
// ── Real-world composition over inheritance: ──────────────────────────
// Instead of: class EmailService extends HttpClient
// Use: EmailService HAS-A HttpClient
public class EmailService {
private final HttpClient httpClient; // composed
private final TemplateEngine templates; // composed
private final Logger logger; // composed (aggregation)
public EmailService(HttpClient httpClient,
TemplateEngine templates,
Logger logger) {
this.httpClient = httpClient;
this.templates = templates;
this.logger = logger;
}
public void sendWelcomeEmail(String to, String name) {
String body = templates.render("welcome", Map.of("name", name));
logger.log("INFO", "Sending welcome to " + to);
httpClient.post("/send",
Map.of("to", to, "body", body));
}
// EmailService IS NOT an HttpClient — it USES one.
// It can switch to a different HttpClient implementation easily.
// It can use multiple templates and log to any logger.Composition for Building Complex Objects
Composition is the primary mechanism for building complex objects from simpler, reusable components. A well-designed system decomposes complex responsibilities into small, focused classes that each do one thing well, and then composes them together to build higher-level abstractions. This is the single responsibility principle applied at the architectural level — each component class has one clear purpose, and the composing class assembles them into a coherent whole.
This approach produces code that is easier to test because each component can be tested independently. It is easier to modify because changing a component only requires changing its class. It is easier to understand because each component is small and focused. And it is easier to reuse because components can be composed in different combinations for different purposes.
The strategy design pattern is a direct application of composition for variability: a Context class composes with a Strategy interface, and different concrete Strategy implementations can be substituted to change the behaviour of the Context without subclassing it. The Template Method pattern in contrast uses inheritance — but the strategy pattern achieves similar extensibility through composition, and is generally preferred for its flexibility.
Java
// ── Complex object built from composed components: ────────────────────
public class OrderProcessor {
// All components created and owned by OrderProcessor — composition:
private final InventoryChecker inventoryChecker;
private final PriceCalculator priceCalculator;
private final PaymentProcessor paymentProcessor;
private final NotificationSender notificationSender;
private final AuditLogger auditLogger;
public OrderProcessor() {
// Composition: OrderProcessor creates and owns all parts:
this.inventoryChecker = new InventoryChecker();
this.priceCalculator = new PriceCalculator();
this.paymentProcessor = new PaymentProcessor();
this.notificationSender = new NotificationSender();
this.auditLogger = new AuditLogger("OrderProcessor");
}
public OrderResult process(Order order) {
auditLogger.log("Processing order: " + order.getId());
// Delegate to each component for its specific responsibility:
if (!inventoryChecker.isAvailable(order.getItems())) {
auditLogger.log("Inventory insufficient for order: " + order.getId());
return OrderResult.failure("Items not available");
}
double total = priceCalculator.calculate(order);
PaymentResult payment = paymentProcessor.charge(
order.getCustomerId(), total);
if (!payment.isSuccess()) {
auditLogger.log("Payment failed for order: " + order.getId());
return OrderResult.failure("Payment declined: " + payment.getReason());
}
notificationSender.sendConfirmation(
order.getCustomerEmail(), order.getId(), total);
auditLogger.log("Order processed successfully: " + order.getId());
return OrderResult.success(order.getId(), total);
}
}
// ── Strategy pattern — composition for variable behaviour: ────────────
public interface SortStrategy {
void sort(int[] array);
String name();
}
public class BubbleSort implements SortStrategy {
@Override
public void sort(int[] arr) { /* bubble sort implementation */ }
@Override public String name() { return "BubbleSort"; }
}
public class MergeSort implements SortStrategy {
@Override
public void sort(int[] arr) { /* merge sort implementation */ }
@Override public String name() { return "MergeSort"; }
}
public class DataSorter {
private SortStrategy strategy; // composed — can be changed at runtime
public DataSorter(SortStrategy strategy) {
this.strategy = strategy;
}
// Strategy can be swapped at runtime — impossible with inheritance:
public void setStrategy(SortStrategy strategy) {
this.strategy = strategy;
}
public void sort(int[] data) {
System.out.println("Sorting with: " + strategy.name());
strategy.sort(data);
}
}
DataSorter sorter = new DataSorter(new BubbleSort());
sorter.sort(new int[]{5, 3, 1, 4, 2});
sorter.setStrategy(new MergeSort()); // swap strategy at runtime
sorter.sort(new int[]{9, 7, 8, 6});Related Topics in Object-Oriented Programming
OOP Concepts
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects — self-contained units that combine data (fields) and behaviour (methods). Java is a class-based, object-oriented language where almost everything is an object. OOP provides four foundational principles: encapsulation, inheritance, polymorphism, and abstraction. Together they produce software that is modular, reusable, maintainable, and easier to reason about as systems grow in complexity.
Classes
A class is the fundamental building block of Java. It is a blueprint that defines the structure and behaviour of objects — what data each object holds (fields) and what operations it can perform (methods). Every Java program is composed of classes. Understanding how to design a class well — choosing the right access modifiers, separating state from behaviour, and writing cohesive single-responsibility classes — is the foundation of object-oriented programming in Java. This entry covers class anatomy, fields, methods, access modifiers, static vs instance members, the this keyword, and class design principles.
Objects
An object is a runtime instance of a class. Where a class is a blueprint that exists in source code, an object is a living entity that exists in memory during program execution — it has its own identity, its own state stored in its fields, and the ability to respond to method calls. Every object has three fundamental properties: identity (a unique memory address), state (the current values of its fields), and behaviour (the methods it responds to). This entry covers object identity vs equality, the Object class hierarchy, object state and mutation, method calls, toString, equals and hashCode, and the object lifecycle.
Object Creation
Object creation in Java is the process of allocating memory, initialising fields, and running constructor logic to bring an object into existence. The new keyword is the primary mechanism, but Java also provides factory methods, builder patterns, copy constructors, and object cloning. Constructors are special methods that set up the initial state — their design determines how easy or difficult the class is to use correctly. This entry covers constructors in depth, constructor overloading and chaining, copy constructors, factory methods, the builder pattern, and the difference between shallow and deep copy.