Back to Blog
Java

Java Inheritance vs Composition: How to Decide

java inheritance vs composition: Learn when to use inheritance vs composition in Java, with code examples, tradeoffs, and practical guidance for maintainable design.

inheritancecompositionobject-oriented designcode reusepolymorphism
A visual comparison of inheritance as a rigid tree structure and composition as a flexible set of connected blocks, illustrating the Java design decision.

A common design decision is whether a new class should reuse behavior by extending a base class or by holding a reference to another object. In Java, extends establishes an is-a relationship, while composition models a has-a relationship. The choice between java inheritance vs composition affects how tightly coupled classes are, how easily behavior can change at runtime, and how much of the parent implementation is exposed. This article compares both approaches with concrete examples and explains the tradeoffs you need to consider.

What Inheritance Gives You in Java

Inheritance lets a subclass inherit fields and methods from a superclass using the extends keyword. The subclass can override methods to change behavior, and it can be used anywhere the superclass is expected, which gives you polymorphism.

public class Animal { public void speak() { System.out.println("Some sound"); } } public class Dog extends Animal { @Override public void speak() { System.out.println("Woof"); } n}

Here, Dog is a specialized Animal. This works well when the relationship is genuinely hierarchical and the the subclass is a strict subtype of the superclass. The cost is that Dog is permanently bound to Animal. You cannot change the parent at runtime, and any change to Animal may ripple through all subclasses. This is the fragile base class problem: a seemingly safe modification to a base class can break subclasses in unexpected ways.

What Composition Offers in Java

Composition models a has-a relationship. Instead of extending a class, you store a reference to another object and delegate calls to it. This gives you more flexibility because you can swap the referenced object at runtime and you control exactly which methods are exposed.

public class Car { private Engine engine; public Car(Engine engine) { this.engine = engine; } public void start() { engine.start(); } }

Here, Car does not inherit from Engine; it uses an Engine instance. The Car class decides what to expose, and the engine can be replaced with a different implementation (e.g., a hybrid engine) without changing the Car class. This level of indirection makes the system more modular and easier to test, because you can inject a mock engine.

When Inheritance Is the Right Choice

Inheritance is appropriate when the relationship is a true is-a and the hierarchy is stable. For example, a Square is a Rectangle, or a SavingsAccount is a BankAccount. The subclass adds or overrides behavior but does not change the fundamental contract of the superclass.

Inheritance also makes sense when you need to leverage polymorphic behavior across a known set of types. For instance, a List<Animal> can hold Dog, Cat, and Bird objects, and calling speak() on each invokes the correct override. This is a natural fit for inheritance when the set of subclasses is fixed at compile time and unlikely to change.

When Composition Is the Right Choice

Composition is the better default in most cases. Use it when the relationship is has-a, such as a Person having an Address, or a Order having a PaymentMethod. Composition also wins when you need to change behavior at runtime, because you can replace the referenced object. It avoids deep inheritance hierarchies that become hard to understand and maintain.

A classic example is the strategy pattern. Instead of creating subclasses for every behavior combination, you compose an object with a strategy interface.

public interface DiscountStrategy { double apply(double price); } public class NoDiscount implements DiscountStrategy { public double apply(double price) { return price; } } public class PercentageDiscount implements DiscountStrategy { private double percent; public PercentageDiscount(double percent) { this.percent = percent; } public double apply(double price) { return price * (1 - percent); } } public class Order { private DiscountStrategy discount; public Order(DiscountStrategy discount) { this.discount = discount; } public double total(double price) { return discount.apply(price); } }

You can switch from NoDiscount to PercentageDiscount at runtime, something inheritance cannot do without creating a new object.

A Concrete Example: Vehicle and Engine

Consider modeling a vehicle with an engine. Using inheritance, you might create a GasCar class that extends Vehicle and hardcodes an engine type. Using composition, you define a Vehicle that holds an Engine reference.

// Inheritance approach public class Vehicle { public void move() { /* ... */ } } public class GasCar extends Vehicle { private GasEngine engine = new GasEngine(); @Override public void move() { engine.start(); // ... } } // Composition approach public class Vehicle { private Engine engine; public Vehicle(Engine engine) { this.engine = engine; } public void move() { engine.start(); // ... } } public class Car extends Vehicle { public Car(Engine engine) { super(engine); } }

In the inheritance version, GasCar is tightly coupled to GasEngine. If you need an electric car, you must create another subclass. In the composition version, you can pass any Engine implementation to Vehicle, and Car simply delegates. The composition version is more flexible and easier to extend without multiplying classes.

Maintainability and Testing Implications

Composition generally leads to more maintainable code because it reduces coupling. When a class depends on an interface rather than a concrete superclass, you can mock that dependency in unit tests. For example, testing Order with a mock DiscountStrategy is trivial. Testing a class that inherits from a complex base class often requires setting up the entire base class hierarchy, which makes tests brittle.

Inheritance also exposes protected members, which can be accidentally overridden or misused by subclasses. Composition hides implementation details behind a well-defined interface, making the contract explicit. This is especially important in large codebases where many developers may edit the same class.

Performance and Runtime Considerations

From a performance standpoint, both approaches have minimal overhead in modern JVMs. Inheritance uses virtual method dispatch, which is optimized by JIT compilation. Composition adds a delegation layer, but the JVM can often inline these calls when the referenced object is known. The real cost difference is rarely measurable in typical applications. What matters more is object creation: composition may require constructing additional objects, but that is usually negligible compared to the benefits of flexibility.

One subtle runtime difference is that inheritance creates a fixed type relationship. You cannot change the superclass at runtime, and the subclass inherits all the state and methods of the parent. Composition allows you to swap the delegate, which can be useful in stateful scenarios. However, this flexibility comes with the responsibility of managing the lifecycle of the composed object.

Common Pitfalls: Fragile Base Class and Deep Hierarchies

The fragile base class problem occurs when a base class is modified and subclasses break because they relied on implementation details. For example, if Animal adds a new method that Dog overrides without realizing it, behavior can change unexpectedly. Composition avoids this by keeping the dependency on an interface, not a concrete class.

Deep inheritance hierarchies are another issue. A class that extends a class that extends another class becomes difficult to understand, test, and debug. Each layer adds indirection and potential for conflict. Composition flattens the design: you combine small, focused objects rather than building a tall tree. This is why many design guidelines recommend favoring composition over inheritance unless there is a clear is-a relationship that is stable over time.

java inheritance vs composition: Practical Usage and Code Ex | RYUSLOG DEV