Back to Blog
Java

Java Composition vs Inheritance: How to Choose

java composition vs inheritance: Compare Java composition and inheritance with concrete examples, tradeoffs, and decision criteria to build maintainable class designs.

compositioninheritanceobject-oriented designJavamaintainabilitydesign patterns
Two building blocks side by side, one stacked vertically to represent inheritance and one containing smaller parts to represent composition, with a Java logo in the background.

The choice between java composition vs inheritance is one of the most frequent design decisions a Java developer faces. Inheritance with extends is built into the language, but composition—holding references to other objects and delegating to them—often produces more flexible and testable code. The right answer depends on the relationship you are modeling and how the code is likely to change.

What Inheritance Gives You and What It Costs

Inheritance lets a subclass reuse the public and protected members of a superclass. It creates an is-a relationship: a Dog is a Animal. The subclass automatically receives the superclass's fields and methods, and it can override them to change behavior.

public class Animal { public void eat() { System.out.println("Eating"); } } public class Dog extends Animal { @Override public void eat() { System.out.println("Dog eating"); } }

The immediate benefit is code reuse and a clear type hierarchy. However, inheritance has a hidden cost: it binds the subclass to the superclass's implementation details. If the superclass changes its internal behavior, subclasses can break without any direct modification. This is the fragile base class problem, and it becomes more serious as the hierarchy grows.

Inheritance also exposes the full public interface of the superclass. A subclass cannot selectively hide methods it does not want to expose. This can violate encapsulation and force subclasses to accept behavior that does not fit their contract.

What Composition Offers

Composition models a has-a relationship. Instead of extending a class, you store an instance of it as a field and delegate calls to it. The composed class controls exactly which methods are exposed and how they behave.

public class Dog { private final Animal animal = new Animal(); public void eat() { animal.eat(); } }

This approach is more verbose, but it gives you precise control. You can choose to delegate only the methods you need, add validation, or replace the delegate at runtime. Because the composed object is just a field, you can swap it with any implementation that satisfies the same interface, which is not possible with inheritance unless you refactor the hierarchy.

Composition also makes testing easier. You can inject a mock delegate into the class under test, something that is difficult with a final or concrete superclass.

Comparing Equivalent Implementations

To see the practical difference, model a Vehicle with an Engine. With inheritance, you might create a Car that extends Vehicle and overrides startEngine(). With composition, you create an Engine interface and have Car hold an Engine reference.

// Inheritance approach public class Vehicle { public void startEngine() { System.out.println("Starting generic engine"); } } public class Car extends Vehicle { @Override public void startEngine() { System.out.println("Starting car engine"); } }
// Composition approach public interface Engine { void start(); } public class Car { private final Engine engine; public Car(Engine engine) { this.engine = engine; } public void start() { engine.start(); } }

The inheritance version hard-codes the engine behavior in the Car class. If you later need a Truck with a different engine, you must create another subclass. The composition version accepts any Engine implementation, so you can reuse the same Car class with a DieselEngine or ElectricEngine by passing a different object to the constructor.

This flexibility is why composition is often preferred in systems that expect change. The cost is a bit more boilerplate, but the reduction in coupling is usually worth it.

When Inheritance Is the Right Choice

Inheritance is appropriate when the relationship is genuinely is-a and the subclass is a strict subtype of the superclass. This means the subclass can be used anywhere the superclass is expected without violating the Liskov Substitution Principle. For example, a Square extending a Rectangle is a classic violation because a square cannot behave like a rectangle in all contexts (setting width independently from height).

Inheritance also works well when the superclass is designed for extension and is not likely to change. Java's AbstractList is a good example: it provides a skeletal implementation, and subclasses only need to implement get() and size(). The base class is stable, and the contract is well-defined.

Another case is when you need to share common behavior across a family of classes and the hierarchy is shallow. A BaseRepository that handles common database operations can be extended by UserRepository and OrderRepository if the base class is carefully designed and tested.

When Composition Is the Better Fit

Composition is the better choice when you need to combine behaviors from multiple sources. Java does not support multiple inheritance of classes, but composition lets you hold several objects and delegate to each. A SmartHomeDevice can have a Camera, a Sensor, and a NetworkModule, each with its own behavior, without forcing a single inheritance chain.

Composition also shines when behavior changes at runtime. If a Player can switch between HumanControl and AIControl, storing a ControlStrategy field and updating it at runtime is natural. Inheritance would require creating a new subclass for every combination, which quickly explodes.

Testing is another strong reason. With composition, you can pass a fake or mock collaborator to isolate the class under test. With inheritance, you often need to override methods or use complex mocking frameworks to simulate superclass behavior.

The Fragile Base Class Problem

A major maintainability concern with inheritance is the fragile base class problem. When a superclass changes its internal implementation, subclasses can break even if their code has not changed. Consider a BaseCollection that adds an element to an internal list in add(). A subclass overrides addAll() to call add() repeatedly. If the base class later changes add() to perform validation that the subclass's addAll() does not expect, the subclass may fail silently.

public class BaseCollection { private List<String> items = new ArrayList<>(); public void add(String item) { items.add(item); } public void addAll(String[] items) { for (String item : items) { add(item); } } } public class CountingCollection extends BaseCollection { private int count; @Override public void add(String item) { count++; super.add(item); } }

If BaseCollection.addAll() is later optimized to use Collections.addAll() directly instead of calling add(), the count in CountingCollection will no longer be incremented. The subclass silently breaks because it relied on an implementation detail of the superclass. Composition avoids this by keeping the base class as a separate object that the subclass delegates to, so changes in the delegate's internals do not affect the wrapper's own state unless explicitly handled.

Runtime and Memory Considerations

Inheritance and composition differ in runtime behavior in subtle ways. Inheritance uses a single object with a vtable for method dispatch. Composition involves an additional object reference and a delegation call, which adds one more level of indirection. In modern JVMs, the performance difference is negligible for most applications. The real cost is in object allocation: composition creates an additional object for each composed field, which can increase memory usage if you create many instances. However, the flexibility and decoupling usually outweigh this small overhead.

Method dispatch in inheritance is resolved through the class hierarchy, which is slightly faster than an interface call in some JVM implementations, but again, the difference is not a practical factor unless you are in a micro-benchmark. Focus on the design tradeoffs rather than micro-optimizations.

Making the Decision in Practice

When you face a design choice, ask whether the relationship is is-a or has-a. If a B is a more specific kind of A and all A behaviors apply to B, inheritance may be appropriate. If a B uses an A to perform a task, composition is usually better.

Consider how likely the base class is to change. If you control the base class and can guarantee its stability, inheritance is safe. If the base class is part of a library or may evolve independently, composition protects your code from unexpected changes.

Think about testability. If you need to mock the collaborator, composition wins. If you are building a small, stable hierarchy and want to avoid boilerplate, inheritance can be more concise.

A common rule of thumb is to favor composition over inheritance unless you have a clear reason for inheritance. This is not a prohibition—it is a warning that inheritance creates tight coupling. In Java, you can often achieve the same code reuse with interfaces and composition, while keeping the ability to change behavior at runtime.

For example, instead of creating a Report class and a PdfReport subclass, define a ReportGenerator interface and have a Report class that holds a ReportGenerator field. This lets you switch between PDF and CSV generation without changing the Report class.

public interface ReportGenerator { String generate(); } public class Report { private final ReportGenerator generator; public Report(ReportGenerator generator) { this.generator = generator; } public String generate() { return generator.generate(); } }

This pattern keeps the Report class focused on its own responsibilities and makes the generation strategy pluggable. Inheritance would force you to create a new subclass for each format, and changing the format at runtime would require a new object.

The final decision should be based on the specific relationship and the expected evolution of the code. If you are unsure, start with composition. You can always refactor to inheritance later if the relationship turns out to be a true subtype, but the reverse is harder because inheritance locks you into a hierarchy that is difficult to unwind.