Back to Blog
Java

Java Has-A vs Is-A: Composition or Inheritance?

java has a vs is a relationship: Understand the difference between has-a and is-a relationships in Java, and learn when to use composition over inheritance for maintai...

compositioninheritanceobject-oriented designjavahas-a relationshipis-a relationship
Diagram showing a class with a has-a relationship to an engine and an is-a relationship to a vehicle.

When modeling a domain in Java, one of the earliest design decisions is whether a class should inherit from another class or hold a reference to it. The phrase java has a vs is a relationship captures this choice: an is-a relationship is implemented with inheritance, while a has-a relationship is implemented with composition. The decision affects coupling, testability, and how easily the code can evolve.

What Is-A Means in Java

An is-a relationship is expressed with the extends keyword. A subclass inherits fields and methods from a superclass, and the subclass is treated as a subtype of the superclass. For example:

public class Animal { public void eat() { System.out.println("Eating..."); } } public class Dog extends Animal { public void bark() { System.out.println("Woof!"); } }

Here Dog is an Animal. This relationship is transitive: if Dog extends Animal, then Dog can be used wherever an Animal is expected. The Java compiler enforces this through the type system, and it is the basis for polymorphism and the Liskov substitution principle.

The key advantage of inheritance is code reuse and a clear hierarchical structure. When the hierarchy is stable and the superclass is well-designed, inheritance can reduce duplication and make the domain model explicit.

What Has-A Means in Java

A has-a relationship is implemented with composition. One class contains a reference to another class as a field. The containing class does not inherit the behavior of the contained class; it delegates to it. For example:

public class Engine { public void start() { System.out.println("Engine started"); } } public class Car { private Engine engine; public Car(Engine engine) { this.engine = engine; } public void start() { engine.start(); } }

Car has an Engine. The Car class does not expose Engine's methods directly unless it defines its own methods that delegate to the engine. This gives the containing class full control over its interface and hides the internal implementation.

Composition is more flexible than inheritance because the relationship is established at runtime. You can swap the Engine implementation without changing the Car class, as long as it satisfies the expected contract (often defined by an interface).

Comparing the Two Relationships

The following table summarizes the practical differences:

CriterionIs-A (Inheritance)Has-A (Composition)
KeywordextendsField reference
Type relationshipSubtype, polymorphicNo subtype relationship
CouplingTight: subclass depends on superclassLoose: depends on interface or class
Runtime flexibilityFixed at compile timeCan change at runtime
Code reuseInherits methods directlyDelegates to contained object
TestabilityHarder to mock superclass behaviorEasy to mock contained dependency

Inheritance creates a compile-time contract that is hard to break. Once a class extends another, every change to the superclass can ripple through the subclass. Composition, on the other hand, allows you to change the contained object's behavior without touching the containing class.

When Inheritance Is the Right Choice

Inheritance is appropriate when the subclass genuinely is a more specific version of the superclass, and the superclass is designed for extension. The relationship should be stable and unlikely to change. For example, in a graphics library, Circle and Rectangle can extend Shape because they share a common interface and the hierarchy is unlikely to change.

Another case is when you need to leverage polymorphic behavior. If you have a method that accepts a List, and you pass an ArrayList, you rely on the is-a relationship between ArrayList and List. This is inheritance through interfaces, which is a form of is-a that is generally safer than class inheritance because interfaces are less likely to change.

Inheritance also makes sense when the superclass provides a concrete template method that subclasses must complete. The Template Method pattern relies on inheritance to define the algorithm skeleton and let subclasses fill in the details.

When Composition Is the Right Choice

Composition is the safer default in most situations. Use it when the relationship is not a clear subtype, or when the behavior of the contained object may vary. For example, a Report class might need to format its output in different ways. Instead of creating subclasses for each format, you can have a Report that holds a Formatter object and delegates formatting to it.

Composition also avoids the fragile base class problem. When a superclass changes its implementation, subclasses can break in subtle ways. With composition, the containing class only depends on the public contract of the contained object, so changes to the contained class are less likely to break the caller.

Another strong reason for composition is testability. If a class inherits from a concrete superclass, you cannot easily mock the superclass's methods in a unit test. With composition, you can inject a mock or a stub for the contained dependency, making the test isolated and fast.

The Fragile Base Class Problem

The fragile base class problem is a well-known issue with inheritance. When a base class is modified, even if the modification is internally consistent, it can break subclasses that rely on the base class's implementation details. For example, consider a Counter class that increments a value and a subclass that overrides increment to also log the value:

public class Counter { private int count; public void increment() { count++; } public int getCount() { return count; } } public class LoggingCounter extends Counter { @Override public void increment() { super.increment(); System.out.println("Count: " + getCount()); } }

If the base class later adds a incrementBy(int) method that internally calls increment(), the subclass's override may not be invoked, leading to inconsistent behavior. This is because the base class does not control how subclasses override its methods. Composition avoids this by delegating to a separate object, so the containing class does not inherit the implementation details.

Composition Over Inheritance in Practice

To apply composition effectively, define an interface for the behavior you need, then inject an implementation. This is the basis of the Strategy pattern. For example:

public interface PaymentProcessor { void processPayment(double amount); } public class CreditCardProcessor implements PaymentProcessor { @Override public void processPayment(double amount) { // process credit card } } public class Order { private PaymentProcessor paymentProcessor; public Order(PaymentProcessor paymentProcessor) { this.paymentProcessor = paymentProcessor; } public void checkout(double amount) { paymentProcessor.processPayment(amount); } }

Here Order has a PaymentProcessor and delegates the payment logic. The Order class is decoupled from the concrete payment implementation, making it easy to test with a mock and to extend with new processors.

When you need to reuse behavior across multiple classes, composition with a shared delegate is often cleaner than a deep inheritance hierarchy. For instance, instead of having Dog and Cat both extend Animal and override makeSound(), you could have each class hold a SoundBehavior object and delegate to it. This allows you to change the behavior at runtime and avoids forcing unrelated classes into a single hierarchy.

Testing and Maintainability Implications

The choice between inheritance and composition has a direct impact on how you test and maintain your code. With inheritance, a unit test for a subclass often requires the superclass's dependencies to be set up. If the superclass has complex initialization, the test becomes brittle. With composition, you can inject a mock or a stub for the contained dependency, keeping the test focused on the class under test.

Maintainability also improves with composition because it reduces coupling. When you change a class that is composed, you only need to ensure it still satisfies the interface used by the containing class. With inheritance, changing a superclass can break all subclasses, even if they did not directly use the changed method. This is why many frameworks and libraries favor composition over inheritance for extensibility.

Common Mistakes and How to Avoid Them

A common mistake is using inheritance to reuse a single method, even when the subclass does not truly share the superclass's contract. For example, a Stack class should not extend Vector just to reuse its methods, because Stack is not a Vector in the semantic sense. This leads to a confusing API and exposes methods that are not appropriate for a stack.

Another mistake is creating deep inheritance hierarchies to model every nuance of a domain. This makes the code hard to follow and change. Instead, favor shallow hierarchies and use composition to add behavior. If you find yourself overriding many methods or checking the type of the subclass, that is a sign that composition would be more appropriate.

Finally, do not use inheritance just to access a private or protected field of the superclass. This violates encapsulation and couples the subclass to the superclass's internal representation. Composition with a well-defined interface is a better way to share state.

Applying the Heuristics in a Real Codebase

When you are designing a new class, ask whether the relationship is truly a subtype. If the class can be substituted for its parent without changing the behavior expected by callers, inheritance is viable. If the class only needs to use the parent's functionality, composition is safer.

Consider the lifecycle of the objects. If the contained object must be replaceable at runtime, composition is the only choice. If the relationship is fixed and the superclass is unlikely to change, inheritance can be simpler and more direct.

A practical approach is to start with composition and only switch to inheritance when you have a clear, stable is-a relationship. This keeps the code flexible and avoids the pitfalls of tight coupling. In Java, interfaces give you the benefits of polymorphism without the fragility of class inheritance, so prefer implementing interfaces over extending classes when possible.

java has a vs is a relationship: Practical Usage and Code Ex | RYUSLOG DEV