Loading learning content...
If IS-A represents inheritance—the taxonomic classification of types—then HAS-A represents composition: building complex objects by containing simpler objects. This is the relationship that models ownership, containment, and assembly.
Where IS-A says "this type is a specialized version of that type," HAS-A says "this type contains that type as a component." The distinction is profound: inheritance creates type identity, while composition creates object structure.
Understanding HAS-A is essential because it offers a fundamentally different—and often superior—approach to code reuse and object construction. While inheritance ties you to a fixed type hierarchy, composition gives you the freedom to assemble behavior at runtime from interchangeable parts.
By the end of this page, you will understand HAS-A as a structural relationship of containment, distinguish between its strong form (composition) and weak form (aggregation), and recognize when HAS-A is the appropriate choice for your design.
The HAS-A relationship signifies that one object contains another object as a part, member, or component. When we say "class A HAS-A class B," we mean that instances of A hold references to instances of B, and A delegates some of its responsibilities to B.
This is fundamentally different from inheritance:
The Containment Model:
In HAS-A, the containing object (the "whole") manages one or more contained objects (the "parts"). The whole:
The parts do their specialized work without knowing about the whole—they're reusable components that can serve different containers.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
// Car HAS-A Engine: The car contains an enginepublic class Engine { private final int horsepower; private boolean running = false; public Engine(int horsepower) { this.horsepower = horsepower; } public void start() { running = true; System.out.println("Engine started: " + horsepower + " HP"); } public void stop() { running = false; System.out.println("Engine stopped"); } public int getHorsepower() { return horsepower; } public boolean isRunning() { return running; }} public class Car { // HAS-A relationship: Car contains an Engine private final Engine engine; private final String model; public Car(String model, Engine engine) { this.model = model; this.engine = engine; // Car HAS-A Engine } public void start() { System.out.println("Starting " + model); engine.start(); // Delegation: Car delegates to Engine } public void stop() { engine.stop(); System.out.println(model + " stopped"); } public int getPower() { return engine.getHorsepower(); // Expose Engine functionality }} // UsageEngine v8 = new Engine(400);Car mustang = new Car("Ford Mustang", v8);mustang.start(); // Ford Mustang starting... Engine started: 400 HPKey Characteristics of HAS-A:
The HAS-A relationship has two flavors with different lifecycle semantics:
Composition (Strong HAS-A):
The contained object's lifecycle is bound to the container. When the container is destroyed, so is the contained object. The part cannot exist independently of the whole.
Examples:
House HAS Rooms (destroy the house, the rooms cease to exist as rooms)Order HAS OrderLines (order lines have no meaning without their order)Brain HAS Neurons (neurons are part of that specific brain)Aggregation (Weak HAS-A):
The contained object exists independently of the container. The container merely uses or references the contained object, but doesn't control its lifecycle.
Examples:
Department HAS Employees (employees exist independently; they can move departments)Playlist HAS Songs (songs exist independently of any playlist)University HAS Professors (professors have their own existence)| Aspect | Composition (Strong) | Aggregation (Weak) |
|---|---|---|
| Lifecycle | Part dies with whole | Part survives whole |
| Ownership | Exclusive ownership | Shared or borrowed reference |
| Creation | Whole creates parts | Parts created externally |
| Sharing | Parts not shared | Parts may be shared |
| Notation (UML) | Filled diamond | Empty diamond |
| Code pattern | Create in constructor | Accept in constructor/setter |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
// COMPOSITION: Room cannot exist without Housepublic class House { private final List<Room> rooms; // House OWNS rooms public House(int numberOfRooms) { // House CREATES the rooms - it controls their lifecycle this.rooms = new ArrayList<>(); for (int i = 0; i < numberOfRooms; i++) { rooms.add(new Room(30 + i * 5, this)); // Rooms belong to THIS house } } public void demolish() { // When house is demolished, rooms are destroyed too rooms.clear(); // Rooms cease to exist as meaningful entities }} // AGGREGATION: Employees exist independently of Departmentpublic class Department { private final String name; private final List<Employee> employees; // Department REFERENCES employees public Department(String name) { this.name = name; this.employees = new ArrayList<>(); } public void addEmployee(Employee emp) { // Employee was created elsewhere and passed in employees.add(emp); } public void removeEmployee(Employee emp) { employees.remove(emp); // Employee continues to exist! They just left this department. } public void close() { employees.clear(); // Employees are removed but NOT destroyed // They can join other departments }} // The distinction matters for resource managementpublic class ResourceExample { public void demonstrateDifference() { // Composition: resources managed internally var house = new House(5); // House creates rooms // When house goes out of scope, rooms are garbage collected together // Aggregation: resources managed externally var emp1 = new Employee("Alice"); // Employee exists first var emp2 = new Employee("Bob"); var engineering = new Department("Engineering"); engineering.addEmployee(emp1); // Alice joins Engineering engineering.addEmployee(emp2); // Bob joins Engineering var sales = new Department("Sales"); sales.addEmployee(emp1); // Alice also in Sales! (shared reference) engineering.close(); // Alice and Bob still exist // They can continue in Sales or join new departments }}Use composition when parts are intrinsically part of the whole and have no meaning outside it (document paragraphs, order items, game character body parts). Use aggregation when parts are independent entities that happen to be associated with the whole (students in a course, books in a library, employees in a team).
While HAS-A establishes the containment structure, delegation is the mechanism that makes it useful. Delegation means forwarding method calls from the containing object to the contained object.
Why Delegation Matters:
Without delegation, contained objects would just be passive data. Delegation transforms HAS-A from mere containment into behavior sharing—the container exposes capabilities of its parts without exposing the parts themselves.
Types of Delegation:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
public class Logger { public void log(String message) { System.out.println("[LOG] " + message); } public void logError(String message) { System.err.println("[ERROR] " + message); } public void logWithTimestamp(String message) { System.out.println("[" + Instant.now() + "] " + message); }} public class PaymentProcessor { private final PaymentGateway gateway; private final Logger logger; // HAS-A Logger public PaymentProcessor(PaymentGateway gateway, Logger logger) { this.gateway = gateway; this.logger = logger; } // Full Forwarding: just call the component method public void log(String message) { logger.log(message); // Straight forwarding } // Partial Forwarding: add logic before/after public PaymentResult processPayment(Payment payment) { logger.log("Processing payment: " + payment.getId()); var result = gateway.process(payment); // Delegate to gateway if (result.isSuccess()) { logger.log("Payment successful"); } else { logger.logError("Payment failed: " + result.getError()); } return result; } // Selective Exposure: only expose what's needed // Notice we DON'T expose logWithTimestamp() - it's not relevant // for PaymentProcessor's clients // Transformation: modify component's output public String getProcessingLog(Payment payment) { // Gateway returns raw events, we transform to readable format List<Event> events = gateway.getEventsFor(payment.getId()); return events.stream() .map(e -> e.getTimestamp() + ": " + e.getDescription()) .collect(Collectors.joining("")); }}Delegation vs Inheritance for Behavior:
Inheritance automatically gives the child all parent behaviors. Delegation requires explicit forwarding for each behavior you want to expose. This seems like more work, but it's actually an advantage:
When you compose an object mainly to add behavior while delegating most methods unchanged, you're implementing the Wrapper (Decorator) pattern. The Decorator extends functionality by composition rather than inheritance, gaining flexibility at the cost of explicit delegation code.
One of HAS-A's greatest strengths over IS-A is runtime flexibility. While inheritance relationships are fixed at compile time, composition relationships can be configured—even changed—at runtime.
Static vs Dynamic Behavior:
With inheritance:
class Dog extends Animal — A Dog is always an Animal, foreverWith composition:
Robot HAS-A MovementStrategy — The strategy can be injected12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
// Strategy interface - defines what we can swappublic interface CompressionStrategy { byte[] compress(byte[] data); byte[] decompress(byte[] data); String getName();} // Different strategies - can be selected at runtimepublic class ZipCompression implements CompressionStrategy { @Override public byte[] compress(byte[] data) { /* ZIP implementation */ } @Override public byte[] decompress(byte[] data) { /* ZIP implementation */ } @Override public String getName() { return "ZIP"; }} public class GzipCompression implements CompressionStrategy { @Override public byte[] compress(byte[] data) { /* GZIP implementation */ } @Override public byte[] decompress(byte[] data) { /* GZIP implementation */ } @Override public String getName() { return "GZIP"; }} public class LZ4Compression implements CompressionStrategy { @Override public byte[] compress(byte[] data) { /* LZ4 implementation */ } @Override public byte[] decompress(byte[] data) { /* LZ4 implementation */ } @Override public String getName() { return "LZ4"; }} // FileArchiver HAS-A CompressionStrategy (via composition)public class FileArchiver { private CompressionStrategy strategy; // Can be changed! public FileArchiver(CompressionStrategy strategy) { this.strategy = strategy; } // Runtime strategy switching! public void setStrategy(CompressionStrategy strategy) { System.out.println("Switching from " + this.strategy.getName() + " to " + strategy.getName()); this.strategy = strategy; } public void archive(List<File> files, Path destination) { System.out.println("Archiving with " + strategy.getName()); for (File file : files) { byte[] compressed = strategy.compress(readFile(file)); writeToArchive(destination, file.getName(), compressed); } } public void extract(Path archive, Path destination) { // Uses current strategy for decompression // ... }} // Usage: behavior changes at runtime!FileArchiver archiver = new FileArchiver(new ZipCompression());archiver.archive(files, archive1); // Uses ZIP archiver.setStrategy(new LZ4Compression()); // Switch to faster algorithmarchiver.archive(files, archive2); // Uses LZ4 // Strategy can be chosen based on runtime conditionsCompressionStrategy strategy = fileSize > LARGE_THRESHOLD ? new LZ4Compression() // Fast for large files : new GzipCompression(); // Better ratio for small filesarchiver.setStrategy(strategy);Implications of Runtime Flexibility:
Dependency injection leverages HAS-A to its fullest. By passing dependencies into constructors (rather than creating them internally), you gain control over what implementations are used—enabling testing with mocks, configuration-driven behavior, and clean separation of concerns.
Unlike inheritance (where most languages allow only single inheritance), composition has no such limitation. An object can have as many components as needed—each providing different capabilities.
Building Complex Objects:
Real-world objects are complex assemblies of many parts. A car has an engine, transmission, chassis, wheels, electronics, etc. Each component is a HAS-A relationship, and together they form the complete car.
Similarly, software objects often need multiple capabilities:
WebApplication HAS-A Router, Database, Cache, Logger, AuthService, etc.Game HAS-A Renderer, PhysicsEngine, AudioSystem, InputHandler, etc.TradingBot HAS-A MarketDataFeed, OrderExecutor, RiskManager, Logger, etc.123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
// A complex object built from multiple componentspublic class SmartHome { // Multiple HAS-A relationships private final SecuritySystem security; private final ClimateControl climate; private final LightingSystem lighting; private final EntertainmentSystem entertainment; private final EnergyMonitor energy; private final List<Sensor> sensors; private final NotificationService notifications; // Constructor injection of all components public SmartHome( SecuritySystem security, ClimateControl climate, LightingSystem lighting, EntertainmentSystem entertainment, EnergyMonitor energy, List<Sensor> sensors, NotificationService notifications) { this.security = security; this.climate = climate; this.lighting = lighting; this.entertainment = entertainment; this.energy = energy; this.sensors = sensors; this.notifications = notifications; } // Coordinate multiple components for complex behaviors public void setAwayMode() { security.arm(); climate.setEcoMode(); lighting.turnOffAll(); entertainment.turnOff(); notifications.send("Home set to away mode"); } public void setHomeMode() { security.disarm(); climate.setComfortMode(); lighting.setWelcomeScene(); } public void handleMotionDetected(Sensor sensor) { if (security.isArmed()) { security.alertMotion(sensor.getLocation()); notifications.sendUrgent("Motion detected: " + sensor.getLocation()); lighting.turnOnAt(sensor.getLocation()); // Illuminate the area } } public EnergyReport getDailyEnergyReport() { return new EnergyReport( energy.getTodayUsage(), climate.getEnergyUsage(), lighting.getEnergyUsage(), entertainment.getEnergyUsage() ); } // Each component can be accessed individually if needed public SecuritySystem getSecurity() { return security; } public ClimateControl getClimate() { return climate; } // ... etc} // This would be IMPOSSIBLE with inheritance// You can't write: class SmartHome extends SecuritySystem, ClimateControl, ...The Coordinator Role:
When an object has many components, it often plays a coordinator role—orchestrating the components to achieve higher-level behaviors. The coordinator:
This is the natural pattern for complex systems: decompose into specialized components, then compose them with a coordinator that manages their interactions.
An object can implement multiple interfaces, which might seem similar to having multiple components. But interfaces only provide TYPE relationships, not behavior. Composition with delegation provides actual reusable BEHAVIOR from each component—the component does the work, not just declares capabilities.
Now that we understand HAS-A, when should we prefer it over IS-A (inheritance)? Here are the key indicators:
Choose HAS-A When:
The Litmus Test:
Ask yourself: "Would it make sense to substitute the whole for the part?"
Also ask: "Does the whole need to be a specialized version of the part?"
We've developed a thorough understanding of HAS-A—the composition relationship that models containment and assembly. Let's consolidate the key insights:
What's Next:
Now that we understand both IS-A (inheritance) and HAS-A (composition), the crucial question becomes: how do we choose between them in practice? The next page provides a systematic framework for testing which relationship applies to any given design scenario—helping you avoid the common mistakes that lead to fragile hierarchies.
You now understand HAS-A as a relationship of containment and assembly, powered by delegation, offering runtime flexibility and enabling complex objects built from multiple components. Next, we'll learn how to systematically test which relationship type applies.