Java Tutorial Series: This lesson is part of our comprehensive Complete Java Tutorial: Learn Java from Beginner to Advanced.

Composition vs Inheritance in Java: Choosing Between Has-A and Is-A Relationships

Composition and inheritance are two major ways to build relationships between Java classes. Inheritance expresses an is-a relationship, while composition expresses a has-a or uses relationship by giving one object a reference to another. Composition is often more flexible because behavior can be changed by replacing collaborating objects rather than changing a class hierarchy.

Inheritance as Is-A

The subclass should honor the expectations established by the parent type.

Composition as Has-A

A class can contain or receive another object and delegate work to it. A Service can have a Repository, for example.

The collaborating object's implementation can often be replaced without changing the service's inheritance hierarchy.

Why Composition Is Flexible

Composition avoids many of the coupling problems caused by deep inheritance. Behavior can be assembled from smaller components.

This is particularly useful when requirements change independently.

Delegation

Delegation means one object asks another object to perform part of a task. It keeps responsibilities separated while allowing a higher-level class to coordinate the workflow.

Dependency Injection

A collaborator can be supplied through a constructor or another injection mechanism. Programming to an interface makes the collaboration easier to replace and test.

Inheritance for Polymorphism

Inheritance remains useful when subtype substitution is meaningful and the parent contract is stable. It can also share implementation when the relationship is genuine.

Composition and Testing

Composed dependencies can be replaced with test doubles or simple implementations. This can make unit tests focused and predictable.

Common Design Smells

Inheritance only to reuse a few methods, subclasses that disable parent behavior and deep hierarchies with many conditional branches are warning signs.

Consider extracting a collaborator or interface.

Performance Considerations

Both approaches have runtime costs that are usually less important than maintainability and correctness. Method calls and object allocation should be measured only when profiling shows a real issue.

Decision Checklist

Ask whether the child truly satisfies the parent contract, whether shared state is required, whether behavior changes independently and whether composition would reduce coupling.

Common Mistakes

Using inheritance because it seems shorter, exposing collaborators unnecessarily and creating too many tiny objects without clear responsibilities are common problems.

Best Practices

Prefer composition when behavior varies independently, use inheritance for genuine subtype relationships and keep dependencies behind focused interfaces where useful.

Practical Examples

Composition

A service receives a repository and delegates data access.

interface Repository { void save(String value); }
class Service {
    private final Repository repository;
    Service(Repository repository) { this.repository = repository; }
    void save(String value) { repository.save(value); }
}

Inheritance

Use when the subtype truly satisfies the parent contract.

class Car extends Vehicle { }
Vehicle v = new Car();

Common Mistakes and Troubleshooting

Most mistakes with object-oriented Java come from confusing a class with an object, misunderstanding which method or constructor is selected, exposing state unnecessarily, or assuming inheritance automatically makes a design better. Debug these problems by checking the declared type, runtime object, constructor path, access level and method-selection rules separately.

  • Test normal, null, boundary and invalid inputs where they apply.
  • Keep responsibilities small so object behavior is easier to understand.
  • Prefer clear contracts over clever inheritance hierarchies.
  • Use @Override when overriding a superclass or interface method.
  • Do not expose mutable internal state without a deliberate reason.

Performance and Design Considerations

Object-oriented design is mainly about modeling behavior and responsibilities, not simply creating many classes. A good design keeps related state and behavior together, reduces unnecessary coupling and makes changes local. In larger Java applications, these choices affect testing, maintenance, dependency management and API stability.

Inheritance, interfaces and composition all have valid uses. The right choice depends on the relationship between the types, the expected variation and how much implementation sharing is actually required. When a topic is performance-sensitive, measure representative workloads rather than assuming that one object-oriented technique is automatically faster.

Practice Exercises

  • Create a small example using the main concept in this article.
  • Add at least one invalid or edge-case input and decide how the program should behave.
  • Compare the concept with one related Java feature and explain the difference.
  • Refactor one large class or method into smaller responsibilities.
  • Write tests for the most important behavior.
  • Explain the design choice as if you were answering an interview question.

Frequently Asked Questions

Is this topic important for Java interviews?

Yes. Core OOP concepts are frequently tested through definitions, code tracing, design questions and practical scenarios.

Should I memorize the syntax?

Learn the syntax through small programs, but focus more on the rule the syntax expresses and when the feature should be used.

Can I use this feature everywhere?

No. Each feature has trade-offs. Prefer the simplest design that clearly represents the requirement.

What should I learn next?

Connect this concept with the surrounding OOP topics, then apply it in a small Java project and test the behavior.

Quick Reference

Concept Core idea Typical use
Inheritance Is-a Stable subtype relationship
Composition Has-a/uses Flexible collaboration
Delegation Passes responsibility Separation of behavior
Injection Supplies dependency Testing and flexibility

Conclusion

The goal of learning this Java OOP concept is not only to recognize its syntax. You should be able to explain how it behaves, identify its boundaries, compare it with related features and use it in a maintainable program. Practice the examples, test edge cases and apply the concept inside a realistic project.

Learn More

  • Continue with the next Java OOP topic and compare the two concepts directly.
  • Practice the examples with different inputs and object relationships.
  • Review classes, objects, collections, exceptions and generics as your programs become larger.

Continue Learning Java

A practical Java path moves from classes and objects into constructors, inheritance, polymorphism, encapsulation, abstraction, interfaces and advanced object-oriented design. Keep each concept connected to a complete program rather than learning isolated definitions.

Deeper Learning Notes

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.

For practical Java development, connect this concept to the application's actual responsibilities and object relationships. Identify what state is owned by the class, which behavior belongs with that state, what callers should be allowed to know, and what implementation details should remain hidden. Compare the feature with at least one alternative, test normal and edge cases, and explain the trade-off. This approach produces code that is easier to review, test and maintain and also prepares you to explain the design clearly in interviews.