Composition over inheritance
On This Lesson 11 topics
Lesson Overview
Inheritance is easy to reach for and easy to misuse. After the last four lessons you can extend a class, call its constructor, override its methods, and close it with final. The skill that matters most now is knowing when not to use extends.
This lesson introduces composition: instead of being a kind of another class, a class holds an object of that class in a field and uses it. In family tree terms, a car is not a child of an engine; the car has an engine under its hood. "Is-a" becomes extends; "has-a" becomes a field.
Why this matters: many real backend designs keep inheritance for a few clear cases and use composition for everything else. Choosing wrongly produces classes that are hard to change and easy to break.
This lesson builds on all earlier lessons in this module and on objects holding references to other objects from the previous module.
Learning Objectives
By the end of this lesson you will be able to:
- Define composition and write a class that holds another object in a field.
- Explain how forwarding (delegation) works.
- Apply the is-a / has-a test to a pair of classes.
- List the signs that inheritance is the wrong tool.
- Explain why composition changes more easily than inheritance.
- Name the main cost of composition.
Written Explanation
Composition: has-a in code
Composition means a class has a field whose type is another class, and its methods use that object to do part of the work.
class Engine {
void ignite() {
System.out.println("Engine running");
}
}
class Car {
private final Engine engine; // has-a
Car(Engine engine) {
this.engine = engine;
}
void start() {
engine.ignite(); // forward the work
}
}new Car(new Engine()).start() prints Engine running.
Calling a method on the held object from your own method is called forwarding or delegation. The Car decides which of the engine's abilities to offer, under which names. A caller of Car sees start(), not ignite().
The two tests
Say both sentences and see which one is true:
| Test | Question | Tool |
|---|---|---|
| is-a | "Is X a kind of Y?" | class X extends Y |
| has-a | "Does X have a Y?" | a field private Y y; |
- A
SavingsAccountis anAccount: inherit. - A
Carhas anEngine: compose. - A
LibraryhasBooks: compose. - An
Orderhas aCustomer: compose.
The test is necessary but not enough. Even when the sentence "X is a Y" is true in English, it must also be true in code: every promise the parent makes must hold for the child. The next section shows a famous case where it is not.
Why "is-a" in maths is not always extends
In geometry a square is a rectangle. In code, give Rectangle independent setWidth and setHeight, and make Square extend it. A square must keep both sides equal, so it overrides both setters to change both sides:
class Rectangle {
protected int width;
protected int height;
void setWidth(int w) { width = w; }
void setHeight(int h) { height = h; }
int area() { return width * height; }
}
class Square extends Rectangle {
@Override
void setWidth(int w) { width = w; height = w; }
@Override
void setHeight(int h) { width = h; height = h; }
}Code written for rectangles expects that setting the width to 5 and the height to 2 gives an area of 10:
static void stretch(Rectangle r) {
r.setWidth(5);
r.setHeight(2);
System.out.println("expected 10, got " + r.area());
}(Passing a Square where a Rectangle is expected is allowed because of the is-a relationship; the next module explains it fully.) The output:
expected 10, got 10
expected 10, got 4A Square does not behave as a Rectangle does, so the promise of inheritance is broken. The honest design is two separate classes, or a Square that has a side and offers area() on its own. "Is-a" in everyday language is a clue. The real test is: can a child be used everywhere the parent is, with no surprises?
Signs that inheritance is the wrong tool
- The child would inherit methods it should not offer. If a
Stackextends a list class, callers can insert in the middle of the stack. (Lesson 7 refactors exactly this.) - The reason for
extendsis "I want that code" and the is-a sentence sounds forced. - The child needs only a few of the parent's methods.
- The hierarchy keeps growing: a fourth or fifth level of children, so that a change at the top ripples through everything.
- The parent is a class you do not control and was not designed for extension.
- You want to change behavior at runtime: a class's parent is fixed when you write it.
Why composition is more flexible
- Narrow surface. The holder chooses which methods to expose. Nobody can call something you did not forward.
- Looser coupling. A child sees the parent's protected parts and depends on how the parent works inside. A holder only depends on the held object's public methods. The parent author can change the inside of a base class and silently break a child; with composition a change to the inside of the held class is usually harmless.
- Swappable parts. The held object is chosen when you construct the holder (or later, if you provide a way). Two
Loggerobjects can hold differentSinks, as in Example 3. A superclass cannot be replaced after the class is written. - No single-parent limit. A class can hold any number of objects, but can extend only one class.
The cost: forwarding code
Composition has a price: you write the forwarding methods yourself, one per capability you want to expose, while inheritance gives them for free. This is the reason beginners overuse extends. Remember that typing a few lines of forwarding is cheap compared to untangling a wrong hierarchy later. The common advice, often summarized as "favor composition over inheritance" and described in the Wikipedia article on the principle, is a guideline to apply selectively, not an absolute ban. Inheritance stays right for a real is-a relationship in a hierarchy designed for it, like SavingsAccount and Account.
Examples
Example 1: Car has an Engine (simple)
The Car/Engine code above. Car extends Engine would be nonsense: a car would inherit ignite() as one of its own public abilities and would be "an engine". Composition says what the world says: the car owns one, uses it, and hides it.
Example 2: Square and Rectangle (practical)
The earlier stretch example shows a hierarchy that compiles and looks right, and produces a wrong area. A composition-style fix keeps Square independent:
class Square {
private int side;
Square(int side) { this.side = side; }
void setSide(int s) { side = s; }
int area() { return side * side; }
}There is no setWidth to misuse and nothing to break. If you really need to share code, Square can hold a Rectangle internally and call rectangle.area(), exposing only its own setSide.
Example 3: a Logger that holds a Sink (realistic)
class Sink {
private final String prefix;
Sink(String prefix) { this.prefix = prefix; }
void write(String line) {
System.out.println(prefix + line);
}
}
class Logger {
private final Sink sink;
private int count = 0;
Logger(Sink sink) { this.sink = sink; }
void log(String message) {
count++;
sink.write("#" + count + " " + message);
}
}In main:
Logger app = new Logger(new Sink("[app] "));
Logger audit = new Logger(new Sink("[AUDIT] "));
app.log("started");
app.log("ready");
audit.log("login");Output:
[app] #1 started
[app] #2 ready
[AUDIT] #1 loginLogger adds numbering and exposes only log. It does not inherit write, so a caller cannot bypass the numbering. Each Logger is given its own Sink. Swapping the destination (a file, a network) later means passing a different sink, with no change to Logger. A Logger extends Sink could not do this: each logger would be bound to one sink design, with all its methods public.
Backend Insight
Services in a backend are mostly composition. A controller class holds a service object; the service holds a repository object that talks to the database. None extends another: each has the next one and calls it. This layering makes testing easy, because you can hand the holder a different, simpler object.
Frameworks still use inheritance in a few places, giving you base classes to extend as plug-in points. The rule of thumb for a professional: inherit when a framework or a real is-a hierarchy asks for it, and compose for everything else. Overusing inheritance is a common reason a codebase gets hard to change. The later modules on design patterns build on this idea.
Common Mistakes
Mistake 1: Inheriting just to reuse code
If the only reason is "I want its methods", hold an object and forward. Inheritance is for is-a.
Mistake 2: Trusting English "is-a" without checking behavior
A square "is a" rectangle in maths. In code, check that every promise of the parent holds for the child.
Mistake 3: Deep hierarchies
Every level adds coupling. If your hierarchy needs four or more levels, stop and look for composition.
Mistake 4: Exposing the held object
A public getter that returns the internal object lets callers bypass your class. Forward the methods you want to offer, and keep the field private.
Mistake 5: Forgetting what the holder gains
Writing a forwarding class that adds nothing, with exactly the same public methods as the held class, gains little. A holder should narrow, add, or combine behavior.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Write "is-a" or "has-a" for each: (a) Dog / Animal, (b) House / Room, (c) Student / Person, (d) Phone / Battery.
Show solution A1
A1. (a) is-a. (b) has-a. (c) is-a. (d) has-a.
A2. What is forwarding?
Show solution A2
A2. A method of the holder calling a method of the held object to do the work.
A3. Give one thing composition gives you that inheritance cannot.
Show solution A3
A3. Any one of: choose a narrow set of exposed methods; swap the held object (a different instance) without changing the holder's code; combine several held objects (a class has only one parent).
Level B: Application
B1. class Teacher extends Address was written because the teacher needs a street and city. Rewrite it as composition: Address has street and city set by its constructor; Teacher holds an Address and offers String where() returning street + ", " + city.
Show solution B1
B1.
class Address {
private final String street;
private final String city;
Address(String street, String city) { this.street = street; this.city = city; }
String where() { return street + ", " + city; }
}
class Teacher {
private final Address address;
Teacher(Address address) { this.address = address; }
String where() { return address.where(); }
}B2. Write Thermostat holding a private Sensor (with double read() returning 21.5) and offering only String status() that returns "Temp: " + sensor.read(). Why can callers not call read() on a Thermostat?
Show solution B2
B2.
class Sensor {
double read() { return 21.5; }
}
class Thermostat {
private final Sensor sensor = new Sensor();
String status() { return "Temp: " + sensor.read(); }
}status() returns Temp: 21.5. Callers cannot call read() because Thermostat does not have it: sensor is private and read was not forwarded.
B3. A team proposes class Invoice extends Customer to reuse the customer's name. Apply both tests and give your verdict.
Show solution B3
B3. is-a: an invoice is not a customer. has-a: an invoice has a customer (or references one). Verdict: do not extend; hold a Customer field.
Level C: Challenge
C1. Redesign the Rectangle/Square pair so no surprise is possible. Describe the classes and say which methods each has.
Show solution C1
C1. Two independent classes. Rectangle has setWidth, setHeight, area. Square has setSide, area. Neither extends the other, so code written for rectangles can never receive a square. Shared helper code can live in a third class both hold.
C2. Count the places in your solution to B2 where a change inside Sensor (say it starts returning int) would break Thermostat, and compare with the effect if Thermostat extends Sensor had been used.
Show solution C2
C2. With composition, only status() in Thermostat uses read(), one place to adjust, and callers of Thermostat are unaffected. With Thermostat extends Sensor, every caller of the inherited read() would break and so would any override of it; the change leaks to everything that uses the child.
Mini Challenge
Take your Logger. Add a second constructor taking no arguments that uses this(new Sink("[default] ")). Then add a method switchTo(Sink s) that replaces the held sink, and make the field non-final to allow it. Log one message before and one after the switch and check the prefixes.
Check your work: the same Logger object printed with two different prefixes, which is the swap-at-runtime benefit of composition.
Summary & Next Lesson
Key takeaways
- Composition (has-a) means holding an object in a private field and forwarding work to it.
- Use
extendsonly for a true is-a relationship where the child can replace the parent with no surprises. - Signs of misused inheritance: unwanted inherited methods, deep hierarchies, "just to reuse code", and parents you do not control.
- Composition exposes only what you choose, is less coupled, and parts can be swapped.
- Its cost is writing the forwarding methods.
Next lesson
The next lesson, "Demo: savings and checking accounts with extends", applies the good case: building SavingsAccount and CheckingAccount from Account step by step, including the planned compile error.
Self-check
- What is composition in one sentence?
- What question distinguishes is-a from has-a?
- Why did
Square extends Rectangleproduce a wrong area? - Name one benefit and one cost of composition.
- Should
Order extends Customerbe used?
Show answers
- A class holds an object in a field and uses it to do part of its work.
- "Is X a kind of Y?" versus "Does X have a Y?"
Squareoverrides the setters so that both sides change, which breaks what rectangle code expects.- Benefit: narrow exposed surface or swappable parts. Cost: forwarding code.
- No. An order has a customer.
References
- Wikipedia, Composition over inheritance. Supports: the principle, the flexibility benefit, the forwarding boilerplate cost, and that it is a selectively applied suggestion. It is a secondary source; I did not reach a primary text such as Effective Java or Design Patterns, and I have not quoted either.
- Oracle, Java Tutorials, Inheritance. Supports the inheritance mechanics used for contrast.
- The Square/Rectangle code, the
Logger/Sinkexample, and all outputs were compiled and run with JDK 21. The analogy and tests are this course's own.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.