Java Composition: Using Has-A Relationships Effectively
java composition: Learn how Java composition models has-a relationships, when to prefer it over inheritance, and how it improves maintainability and testability.
java composition requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
In Java, composition models a has-a relationship by storing another object as a field. It is the primary alternative to inheritance for code reuse and is often the better choice when you want to keep your object model flexible and avoid deep class hierarchies. This article explains how composition works, when to prefer it over inheritance, and how to implement it cleanly in real Java code.
What Composition Means in Java
Composition is a design principle where a class contains one or more instances of other classes as fields. The containing class delegates behavior to those contained objects. For example, a Car class might contain an Engine object and a Transmission object. The car has an engine, and it has a transmission. That is a has-a relationship.
In contrast, inheritance models an is-a relationship. A SportsCar extends Car, meaning it is a specialized kind of car. Composition does not create a subtype; it creates a dependency between two independent classes.
The key difference is that composition gives you the ability to change the behavior of a class by swapping the contained object at runtime, while inheritance fixes the relationship at compile time.
Implementing Composition with Plain Fields
The simplest form of composition is a field with a concrete type. Consider an Engine class:
public class Engine { public void start() { System.out.println("Engine started"); } }
Now a Car class can hold an Engine:
public class Car { private final Engine engine; public Car(Engine engine) { this.engine = engine; } public void start() { engine.start(); } }
The Car delegates its start() method to the Engine. This is straightforward composition. The Car does not need to know how the engine starts; it only knows that it has an engine that can be started.
You can also use interfaces to make the composition more flexible. Instead of depending on a concrete Engine, define an Engine interface and allow different implementations:
public interface Engine { void start(); } public class ElectricEngine implements Engine { @Override public void start() { System.out.println("Electric motor started"); } } public class GasEngine implements Engine { @Override public void start() { System.out.println("Gas engine started"); } }
Then Car can accept any Engine implementation:
public class Car { private final Engine engine; public Car(Engine engine) { this.engine = engine; } public void start() { engine.start(); } }
Now you can create a car with an electric engine or a gas engine without changing the Car class. This is a direct benefit of composition: the behavior is injected rather than hard-coded.
Composition vs Inheritance: The Core Tradeoff
Inheritance is attractive because it is simple to write. You extend a base class and override methods. But it creates a tight coupling between the subclass and the superclass. Changes to the superclass can silently affect all subclasses. This is known as the fragile base class problem.
Composition avoids that problem by keeping classes independent. The composed class only knows the interface or contract of the contained object, not its internal implementation.
Consider a Bird class that can fly. If you want a Penguin that cannot fly, inheritance forces you to either override fly() to do nothing or throw an exception. With composition, you can define a FlyBehavior interface and give each bird the behavior it needs:
public interface FlyBehavior { void fly(); } public class CanFly implements FlyBehavior { public void fly() { System.out.println("Flying"); } } public class CannotFly implements FlyBehavior { public void fly() { System.out.println("Cannot fly"); } } public class Bird { private final FlyBehavior flyBehavior; public Bird(FlyBehavior flyBehavior) { this.flyBehavior = flyBehavior; } public void performFly() { flyBehavior.fly(); } }
Now a penguin can be created with CannotFly, and an eagle with CanFly. The behavior is decoupled from the bird type.
The table below summarizes the main differences:
| Aspect | Composition | Inheritance |
|---|---|---|
| Relationship | Has-a | Is-a |
| Coupling | Loose, via interfaces | Tight, via class hierarchy |
| Runtime change | Possible by swapping objects | Not possible |
| Code reuse | Through delegation | Through method inheritance |
| Testability | Easy to mock dependencies | Harder to isolate superclass |
Inheritance is still useful when you have a true subtype relationship and you want to share common implementation. But for most code reuse scenarios, composition gives you more control and fewer hidden dependencies.
When Composition Is the Better Choice
Composition is the better choice when you need to vary behavior at runtime, when you want to avoid deep class hierarchies, or when you are working with classes that do not have a natural is-a relationship.
A common rule of thumb is to prefer composition over inheritance unless there is a clear is-a relationship that will not change. For example, a Dog is a Mammal, so inheritance may be appropriate. But a UserService does not need to extend a DatabaseService; it should contain a DatabaseService as a field.
Composition also makes it easier to follow the single responsibility principle. Each class can focus on one task, and you combine them to build complex behavior. This leads to smaller, more focused classes that are easier to test.
Another scenario is when you need to add behavior to a class without modifying it. With composition, you can wrap an existing object in a decorator. The decorator implements the same interface and delegates to the wrapped object while adding extra behavior.
public interface Notifier { void send(String message); } public class EmailNotifier implements Notifier { public void send(String message) { System.out.println("Sending email: " + message); } } public class LoggingNotifier implements Notifier { private final Notifier wrapped; public LoggingNotifier(Notifier wrapped) { this.wrapped = wrapped; } public void send(String message) { System.out.println("Logging before send"); wrapped.send(message); System.out.println("Logging after send"); } }
This is a classic use of composition for cross-cutting concerns like logging, caching, or security.
Common Composition Pitfalls
Composition is not without its own traps. One common mistake is exposing the internal object directly through a getter. If you return the contained object, callers can modify its state in unexpected ways, breaking encapsulation. Instead, provide methods that delegate only the necessary operations.
Another pitfall is creating deep composition chains that make the code hard to follow. If a class contains a class that contains another class, and the method call passes through multiple layers, debugging becomes difficult. Keep the delegation shallow and make sure each layer adds clear value.
A third issue is forgetting to handle null dependencies. If a composed object is required for the class to work, you should enforce that through the constructor and validate it. Using Objects.requireNonNull is a simple way to fail fast:
public Car(Engine engine) { this.engine = Objects.requireNonNull(engine, "engine must not be null"); }
This prevents a NullPointerException later in an unexpected location.
Composition and Dependency Injection
Composition naturally aligns with dependency injection. Instead of a class creating its dependencies internally, you pass them in through the constructor. This is exactly what the Car example does. The Car receives its Engine from outside, which makes the relationship explicit and testable.
Constructor injection is the most common form because it makes the required dependencies clear and ensures the object is fully initialized before use. You can also use setter injection or method injection, but constructor injection is generally preferred for mandatory dependencies.
With composition, you can easily swap implementations in tests. For example, you can pass a mock Engine to a Car and verify that start() calls engine.start() without actually starting a real engine. This is much harder with inheritance because you would need to mock the superclass behavior.
Testing and Maintainability with Composition
Composition improves testability because each composed class can be tested in isolation. The containing class can be tested with fake or mock dependencies, so you do not need to set up the entire object graph.
Maintainability also improves because changes to a contained class do not ripple through the hierarchy. If the Engine interface changes, you only need to update the Car class if the method signature changes. With inheritance, a change to a superclass method can break every subclass that overrides it.
Composition also makes it easier to follow the open/closed principle. You can extend behavior by adding new implementations of an interface and injecting them, without modifying existing classes. This keeps your code open for extension but closed for modification.
When you design a class, ask whether it is a type of something or whether it has something. If the answer is "has", use composition. This simple decision leads to more flexible, maintainable code in most real-world Java applications.