One of the foundational principles in Object-Oriented Design (OOD) is understanding the relationships between objects. Two primary types of relationships often discussed are “is-a” and “has-a”. This section focuses on the “has-a” relationship, which plays a crucial role in designing flexible, maintainable systems by favoring composition over inheritance.
A “has-a” relationship describes a situation where one object contains or owns another object as part of its state or behavior. It models real-world entities by capturing the fact that some objects are made up of or possess other objects. This kind of relationship is also known as composition or aggregation, depending on the strength of the relationship.
For example:
These relationships reflect part-whole hierarchies where the whole object “has” one or more parts.
Understanding the difference between “has-a” (composition) and “is-a” (inheritance) relationships is essential for sound design:
“Is-a” (Inheritance): When an object is a specialized type of another, it uses inheritance. For example, a SportsCar
is a Car
, so it inherits properties and behaviors from Car
. The relationship indicates that the subclass is a kind of the superclass.
“Has-a” (Composition): When an object contains or is composed of other objects, it uses composition. For example, a Car
has an Engine
. The car is not a type of engine but contains one.
In summary:
While inheritance is powerful, relying too heavily on it can lead to rigid, tightly coupled designs. In contrast, composition via “has-a” relationships offers greater flexibility, modularity, and reuse. Here’s why:
Here are some simple Java examples illustrating “has-a” relationships:
class Engine {
void start() {
System.out.println("Engine started.");
}
}
class Car {
private Engine engine; // Car "has-a" Engine
public Car() {
this.engine = new Engine();
}
public void startCar() {
engine.start(); // Delegates behavior to the engine
System.out.println("Car is ready to go!");
}
}
In this example, the Car
class has an Engine
object. The car delegates engine-related tasks to the engine object rather than inheriting from it.
Another example:
class Room {
private String name;
public Room(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class House {
private Room[] rooms; // House "has-a" collection of Rooms
public House(Room[] rooms) {
this.rooms = rooms;
}
public void listRooms() {
for (Room room : rooms) {
System.out.println("Room: " + room.getName());
}
}
}
The House
object contains an array of Room
objects, showing a “has-a” relationship with multiple components.
class Engine {
void start() {
System.out.println("Engine started.");
}
}
class Car {
private Engine engine; // Car "has-a" Engine
public Car() {
this.engine = new Engine();
}
public void startCar() {
engine.start(); // Delegates behavior to the engine
System.out.println("Car is ready to go!");
}
}
class Room {
private String name;
public Room(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class House {
private Room[] rooms; // House "has-a" collection of Rooms
public House(Room[] rooms) {
this.rooms = rooms;
}
public void listRooms() {
for (Room room : rooms) {
System.out.println("Room: " + room.getName());
}
}
}
public class HasARelationshipDemo {
public static void main(String[] args) {
Car car = new Car();
car.startCar();
Room[] rooms = { new Room("Living Room"), new Room("Bedroom"), new Room("Kitchen") };
House house = new House(rooms);
house.listRooms();
}
}
When designing software, try to think in terms of how objects are composed of other objects rather than forcing everything into an inheritance hierarchy. Ask yourself:
By focusing on “has-a” relationships, your designs will tend to be more adaptable to change, easier to test, and clearer to understand.
Mastering these concepts will help you design better, more maintainable object-oriented systems as you progress through this book.
In object-oriented design, two fundamental ways to model relationships between classes are inheritance and composition. Both techniques allow one class to reuse or extend the behavior of another, but they do so in very different ways, each with distinct advantages and trade-offs. Understanding when to use each is crucial to designing flexible, maintainable, and reusable Java software.
Inheritance is a mechanism where one class (called the subclass or child class) inherits fields and methods from another class (the superclass or parent class). It represents an “is-a” relationship: the subclass is a specialized type of the superclass.
For example, consider a class hierarchy where Car
extends Vehicle
:
class Vehicle {
void start() {
System.out.println("Vehicle starting");
}
}
class Car extends Vehicle {
void openTrunk() {
System.out.println("Opening trunk");
}
}
Here, Car
inherits the start()
method from Vehicle
and adds its own behavior.
Composition models a “has-a” relationship where one class contains an instance of another as a component. Instead of inheriting behavior, the containing class delegates tasks to the component objects it holds.
Using the same example:
class Engine {
void start() {
System.out.println("Engine starting");
}
}
class Car {
private Engine engine;
public Car() {
engine = new Engine();
}
void start() {
engine.start();
System.out.println("Car is ready to go");
}
}
Here, Car
has an Engine
, and it uses that engine to perform the start operation.
public class Main {
public static void main(String[] args) {
System.out.println("Inheritance example:");
CarInheritance car1 = new CarInheritance();
car1.start(); // Inherited from Vehicle
car1.openTrunk(); // Defined in CarInheritance
System.out.println("\nComposition example:");
CarComposition car2 = new CarComposition();
car2.start(); // Delegates to Engine
}
}
// Inheritance example
class Vehicle {
void start() {
System.out.println("Vehicle starting");
}
}
class CarInheritance extends Vehicle {
void openTrunk() {
System.out.println("Opening trunk");
}
}
// Composition example
class Engine {
void start() {
System.out.println("Engine starting");
}
}
class CarComposition {
private Engine engine;
public CarComposition() {
engine = new Engine();
}
void start() {
engine.start();
System.out.println("Car is ready to go");
}
}
Aspect | Inheritance | Composition |
---|---|---|
Relationship | “Is-a” — subclass is a specialized type of superclass | “Has-a” — class contains component objects |
Coupling | Tight coupling to superclass implementation | Loose coupling; components are replaceable |
Flexibility | Less flexible; changes in superclass affect subclasses | More flexible; components can be changed or replaced independently |
Reuse | Reuse by inheriting behavior and overriding methods | Reuse by delegating behavior to composed objects |
Maintenance | Can lead to fragile base classes if superclass changes | Easier to maintain as components are independent |
Multiple inheritance | Java does not support multiple class inheritance | Allows mixing different components freely |
Pros:
Cons:
Pros:
Cons:
Suppose you want to model different types of payment processing.
Inheritance example:
class PaymentProcessor {
void processPayment(double amount) {
System.out.println("Processing payment: " + amount);
}
}
class CreditCardProcessor extends PaymentProcessor {
@Override
void processPayment(double amount) {
System.out.println("Processing credit card payment: " + amount);
}
}
Composition example:
interface PaymentMethod {
void pay(double amount);
}
class CreditCard implements PaymentMethod {
public void pay(double amount) {
System.out.println("Paying with credit card: " + amount);
}
}
class PaymentProcessor {
private PaymentMethod paymentMethod;
public PaymentProcessor(PaymentMethod paymentMethod) {
this.paymentMethod = paymentMethod;
}
void processPayment(double amount) {
paymentMethod.pay(amount);
}
}
In the composition example, PaymentProcessor
can work with any PaymentMethod
implementation, making it flexible and extensible without changing the processor class.
Modern object-oriented design strongly advocates “favor composition over inheritance”. The rationale is:
This does not mean inheritance is bad—it's still valuable for clear “is-a” relationships and polymorphism. However, composition should be the default choice when designing relationships between classes.
Understanding these concepts deeply equips you to make better design choices as you build complex, scalable object-oriented systems.
In software development, code reuse is a key goal—it saves time, reduces errors, and promotes consistency across projects. Designing classes and components for reuse requires thoughtful planning, especially when using composition, which is the preferred way to build flexible and maintainable systems.
Unlike inheritance, which tightly couples subclasses to their parent classes, composition enables reuse by assembling independent, well-defined components. This modular approach makes it easier to reuse and extend parts of a system without affecting unrelated areas.
For example, a logging component designed as a standalone class can be reused across many different parts of an application or even across different projects simply by including and configuring it, rather than forcing all clients to inherit from a base class that provides logging.
Design classes to have one clear responsibility. When a class does one thing well, it becomes easier to understand, test, and reuse in different contexts.
class Logger {
void log(String message) {
System.out.println("LOG: " + message);
}
}
This Logger
class is simple and focused—perfect for reuse anywhere logging is needed.
Define components using interfaces rather than concrete classes. Interfaces specify what a component does without prescribing how it does it. This abstraction enables multiple implementations that can be swapped easily.
interface PaymentMethod {
void pay(double amount);
}
class CreditCardPayment implements PaymentMethod {
public void pay(double amount) {
System.out.println("Paying $" + amount + " by credit card");
}
}
By depending on PaymentMethod
rather than CreditCardPayment
directly, your system becomes more flexible and reusable.
Build complex behavior by composing small, reusable components. Use delegation to forward requests to component objects, avoiding the pitfalls of inheritance.
Example: A Car
class composed of an Engine
and Transmission
:
class Engine {
void start() { System.out.println("Engine started"); }
}
class Transmission {
void shiftGear(int gear) { System.out.println("Shifted to gear " + gear); }
}
class Car {
private Engine engine = new Engine();
private Transmission transmission = new Transmission();
void drive() {
engine.start();
transmission.shiftGear(1);
System.out.println("Car is driving");
}
}
Each component is reusable on its own, and Car
simply composes these parts.
Immutable objects (objects whose state cannot change after construction) are naturally reusable because they are thread-safe and side-effect free.
By designing these components with clear interfaces and minimal dependencies, you enable reuse across many parts of your application or even across projects.
Several well-known design patterns explicitly promote composition to maximize reuse:
interface Coffee {
double cost();
}
class SimpleCoffee implements Coffee {
public double cost() { return 2.0; }
}
class MilkDecorator implements Coffee {
private Coffee coffee;
public MilkDecorator(Coffee coffee) { this.coffee = coffee; }
public double cost() { return coffee.cost() + 0.5; }
}
interface SortingStrategy {
void sort(int[] array);
}
class BubbleSort implements SortingStrategy {
public void sort(int[] array) { /* bubble sort implementation */ }
}
class QuickSort implements SortingStrategy {
public void sort(int[] array) { /* quick sort implementation */ }
}
class Sorter {
private SortingStrategy strategy;
public Sorter(SortingStrategy strategy) { this.strategy = strategy; }
public void sortArray(int[] array) { strategy.sort(array); }
}
interface SortingStrategy {
void sort(int[] array);
}
class BubbleSort implements SortingStrategy {
public void sort(int[] array) {
// Simple bubble sort implementation
int n = array.length;
for(int i = 0; i < n-1; i++) {
for(int j = 0; j < n-i-1; j++) {
if(array[j] > array[j+1]) {
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
}
}
class QuickSort implements SortingStrategy {
public void sort(int[] array) {
quickSort(array, 0, array.length - 1);
}
private void quickSort(int[] arr, int low, int high) {
if(low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi -1);
quickSort(arr, pi + 1, high);
}
}
private int partition(int[] arr, int low, int high) {
int pivot = arr[high];
int i = (low - 1);
for(int j = low; j < high; j++) {
if(arr[j] < pivot) {
i++;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
int temp = arr[i+1];
arr[i+1] = arr[high];
arr[high] = temp;
return i + 1;
}
}
class Sorter {
private SortingStrategy strategy;
public Sorter(SortingStrategy strategy) {
this.strategy = strategy;
}
public void sortArray(int[] array) {
strategy.sort(array);
}
}
public class MainSort {
public static void main(String[] args) {
int[] data = {5, 3, 8, 1, 2};
Sorter bubbleSorter = new Sorter(new BubbleSort());
bubbleSorter.sortArray(data);
System.out.print("BubbleSorted array: ");
for(int num : data) System.out.print(num + " ");
System.out.println();
data = new int[]{5, 3, 8, 1, 2}; // reset array
Sorter quickSorter = new Sorter(new QuickSort());
quickSorter.sortArray(data);
System.out.print("QuickSorted array: ");
for(int num : data) System.out.print(num + " ");
System.out.println();
}
}
Designing for reuse with composition involves:
By following these strategies, you build software that is easier to maintain, extend, and adapt—key qualities for modern, scalable Java applications.
In this section, we will explore a practical, real-world example of composition by modeling a Car
composed of several smaller components such as an Engine
, Wheel
, and Transmission
. This example will demonstrate how objects can be assembled from other objects to build flexible and maintainable designs.
A car is not just one big object; it is made up of many parts working together. Instead of trying to cram all functionality into one monolithic class, composition allows us to break down the problem into manageable, reusable pieces.
By modeling a car as a has-a relationship—i.e., a car has an engine, has wheels, has a transmission—we reflect the real-world relationships clearly and create components that can be developed and tested independently.
First, let’s define each component class with simple functionality.
// Engine.java
public class Engine {
private boolean running;
public void start() {
running = true;
System.out.println("Engine started.");
}
public void stop() {
running = false;
System.out.println("Engine stopped.");
}
public boolean isRunning() {
return running;
}
}
Wheel.java
public class Wheel {
private String position;
public Wheel(String position) {
this.position = position;
}
public void rotate() {
System.out.println(position + " wheel is rotating.");
}
}
Transmission.java
public class Transmission {
private int currentGear = 0;
public void shiftGear(int gear) {
currentGear = gear;
System.out.println("Shifted to gear " + currentGear);
}
public int getCurrentGear() {
return currentGear;
}
}
The Car
class has an Engine
, multiple Wheel
objects, and a Transmission
. We compose these inside Car
as fields and use their behavior to implement the car's functionality.
Car.java
public class Car {
private Engine engine;
private Wheel[] wheels;
private Transmission transmission;
public Car() {
// Initialize components
engine = new Engine();
wheels = new Wheel[] {
new Wheel("Front Left"),
new Wheel("Front Right"),
new Wheel("Rear Left"),
new Wheel("Rear Right")
};
transmission = new Transmission();
}
public void startCar() {
engine.start();
transmission.shiftGear(1);
for (Wheel wheel : wheels) {
wheel.rotate();
}
System.out.println("Car is started and ready to drive.");
}
public void stopCar() {
transmission.shiftGear(0);
engine.stop();
System.out.println("Car has stopped.");
}
// Additional behaviors can be added here...
}
Let’s create a Main
class to run this car simulation.
// Main.java
public class Main {
public static void main(String[] args) {
Car myCar = new Car();
myCar.startCar();
// Simulate driving...
myCar.stopCar();
}
}
Expected Console Output:
Engine started.
Shifted to gear 1
Front Left wheel is rotating.
Front Right wheel is rotating.
Rear Left wheel is rotating.
Rear Right wheel is rotating.
Car is started and ready to drive.
Shifted to gear 0
Engine stopped.
Car has stopped.
// Engine.java
class Engine {
private boolean running;
public void start() {
running = true;
System.out.println("Engine started.");
}
public void stop() {
running = false;
System.out.println("Engine stopped.");
}
public boolean isRunning() {
return running;
}
}
// Wheel.java
class Wheel {
private String position;
public Wheel(String position) {
this.position = position;
}
public void rotate() {
System.out.println(position + " wheel is rotating.");
}
}
// Transmission.java
class Transmission {
private int currentGear = 0;
public void shiftGear(int gear) {
currentGear = gear;
System.out.println("Shifted to gear " + currentGear);
}
public int getCurrentGear() {
return currentGear;
}
}
// Car.java
class Car {
private Engine engine;
private Wheel[] wheels;
private Transmission transmission;
public Car() {
engine = new Engine();
wheels = new Wheel[] {
new Wheel("Front Left"),
new Wheel("Front Right"),
new Wheel("Rear Left"),
new Wheel("Rear Right")
};
transmission = new Transmission();
}
public void startCar() {
engine.start();
transmission.shiftGear(1);
for (Wheel wheel : wheels) {
wheel.rotate();
}
System.out.println("Car is started and ready to drive.");
}
public void stopCar() {
transmission.shiftGear(0);
engine.stop();
System.out.println("Car has stopped.");
}
}
// Main.java
public class Main {
public static void main(String[] args) {
Car myCar = new Car();
myCar.startCar();
// Simulate driving...
myCar.stopCar();
}
}
Modularity: Each component (Engine
, Wheel
, Transmission
) encapsulates its own behavior and state. This modularity makes it easier to maintain and enhance individual parts without affecting the whole car.
Reusability: The same Engine
or Transmission
classes could be reused in other vehicle types like trucks or motorcycles with little or no modification.
Flexibility: If we want to model different types of wheels or engines (e.g., electric vs combustion), we can subclass or swap components without changing the Car
class’s overall structure.
Clear Responsibilities: Each class has a clear responsibility—Engine
starts and stops, Wheel
rotates, Transmission
shifts gears—promoting better separation of concerns.
Testability: You can unit test each component independently before integrating them into the Car
. This reduces bugs and improves reliability.
This example showcases how composition models real-world “has-a” relationships effectively in Java. Instead of one large class trying to do everything, breaking down a complex object like a Car
into components leads to a cleaner, more manageable design.
Composition encourages building systems by assembling smaller parts with defined roles—making code easier to understand, extend, and maintain. This principle is central to modern object-oriented design and is preferred over inheritance when modeling “part-of” relationships.