Inheritance is seductive: declare Penguin extends Bird and your penguin immediately has every field and method a bird has, for free. That 'for free' is the catch. A penguin is genuinely a bird, but it cannot fly — and because move() lives on the parent, the penguin silently inherits a method that prints flies. The compiler is perfectly happy; the bug is semantic, and it ships.
This is the core weakness of building with inheritance alone: you inherit the whole public surface of the parent, the methods you wanted and the ones you did not, bundled together. Composition is the alternative. Instead of being a thing, your object has a thing — it holds another object in a field and hands work to it. This lesson shows why that distinction matters and how to predict what a delegated call does.
flowchart LR B["Bird: move = flies"] -->|"extends"| P["Penguin: inherits move"] P -.->|"gets a method it cannot do"| X["penguin prints flies — wrong"] style X fill:#b45309,color:#fff
public class Main {
static class Bird {
String move() {
return "flies";
}
}
static class Penguin extends Bird {
}
public static void main(String[] args) {
Bird[] birds = { new Bird(), new Penguin() };
for (Bird b : birds) {
System.out.println(b.move());
}
}
}
Has-a beats is-a for borrowing behaviour
In composition, a class holds a reference to another object and asks it to do the work — a pattern called delegation. An Animal no longer is a movement; it has a movement, supplied at construction. A pigeon is built with a flying movement, a penguin with a walking one, and the same Animal class serves both with no inheritance gymnastics.
The win is precision. You pull in exactly the behaviour you ask for and nothing else, because you chose the field's type yourself. Swapping behaviour is a constructor argument away, and you can combine several independent capabilities inside one object — something single inheritance forbids. When you catch yourself writing extends only to reuse a single method, that is the moment to reach for a field instead.
flowchart LR A["Animal"] -->|"holds a"| M["Movement field"] M -->|"delegates the call to"| F["WalkingMovement or FlyingMovement"] style A fill:#3776ab,color:#fff style M fill:#c2410c,color:#fff
public class Main {
interface Movement {
String move();
}
static class FlyingMovement implements Movement {
@Override
public String move() {
return "flies";
}
}
static class WalkingMovement implements Movement {
@Override
public String move() {
return "walks";
}
}
static class Animal {
private final Movement movement;
Animal(Movement movement) {
this.movement = movement;
}
String move() {
return movement.move();
}
}
public static void main(String[] args) {
Animal pigeon = new Animal(new FlyingMovement());
Animal penguin = new Animal(new WalkingMovement());
System.out.println(pigeon.move());
System.out.println(penguin.move());
}
}
Tracing a delegation
A delegated call is mundane but worth reading slowly. Animal stores its Movement in a private final field, and its own move() method does nothing clever — it returns movement.move(), forwarding the call straight to the object it holds. When you read penguin.move(), two hops happen: first into Animal.move(), then into whichever movement object the penguin was constructed with.
That second hop is ordinary method dispatch on a field; there is no new rule to learn. What changes is where the behaviour lives. With inheritance the behaviour is glued into the class hierarchy forever; with composition it is an object you chose, which means you can replace it, test it in isolation, or hand in a different one per instance.
flowchart TD S["Service"] --> L["Logger capability"] S --> V["Saver capability"] style S fill:#3776ab,color:#fff
public class Main {
interface Logger {
String log(String msg);
}
interface Saver {
String save(String msg);
}
static class ConsoleLogger implements Logger {
@Override
public String log(String msg) {
return "logged: " + msg;
}
}
static class FileSaver implements Saver {
@Override
public String save(String msg) {
return "saved: " + msg;
}
}
static class Service {
private final Logger logger;
private final Saver saver;
Service(Logger logger, Saver saver) {
this.logger = logger;
this.saver = saver;
}
String handle(String msg) {
return logger.log(msg) + " | " + saver.save(msg);
}
}
public static void main(String[] args) {
Service s = new Service(new ConsoleLogger(), new FileSaver());
System.out.println(s.handle("hello"));
}
}
When inheritance is the right call
Composition is not a blanket rule; inheritance is correct when the relationship is genuine identity. A SavingsAccount really is an Account — it shares the account's state, its invariants, and its whole reason to exist, so extending it reads naturally and breaks nothing. The smell to watch for is using extends merely to borrow a method from a class you are not truly a kind of, dragging in an is-a relationship you do not actually mean.
A useful test: can you say the subclass is a parent, in plain English, without hedging? If yes, inheritance fits. If you find yourself saying the subclass merely uses a behaviour the parent happens to have, reach for a field. Inheriting identity is sound design; inheriting to steal a single method is a trap that hardens over time.
What does this program print? A Courier holds a Transport and delegates. Predict both lines.
public class Main {
interface Transport {
String deliver();
}
static class Truck implements Transport {
@Override
public String deliver() { return "by road"; }
}
static class Drone implements Transport {
@Override
public String deliver() { return "by air"; }
}
static class Courier {
private final Transport transport;
Courier(Transport transport) {
this.transport = transport;
}
String ship() {
return "ships " + transport.deliver();
}
}
public static void main(String[] args) {
Courier a = new Courier(new Drone());
Courier b = new Courier(new Truck());
System.out.println(a.ship());
System.out.println(b.ship());
}
}
awas built with aDrone, sotransport.deliver()returns"by air".bwas built with aTruck, sotransport.deliver()returns"by road".ship()prepends"ships "to whichever value the delegate returns.
A class needs to log messages and also to cache results, and logging and caching live in two unrelated existing classes. Which approach fits best?
Logging and caching are capabilities your class has, not things it is. Composition lets one object hold both, and a class can extend only a single parent anyway — so inheritance cannot combine two unrelated bases here.
The inheritance trap. Circle extends Shape and overrides nothing. What does a circle report for corners?
public class Main {
static class Shape {
int corners() {
return 4;
}
}
static class Circle extends Shape {
}
public static void main(String[] args) {
Shape s = new Circle();
System.out.println(s.corners());
}
}
Circleoverrides nothing, so it inheritsShape.corners()unchanged.A circle truly has 0 corners — yet the inherited method returns 4.
That mismatch is exactly why inheriting to borrow behaviour is risky.
Swappable delegate. The Unit starts in one mode, then switches at runtime. Predict both lines.
public class Main {
interface Mode {
String act();
}
static class AttackMode implements Mode {
@Override
public String act() { return "attacks"; }
}
static class DefendMode implements Mode {
@Override
public String act() { return "defends"; }
}
static class Unit {
private Mode mode;
Unit(Mode mode) {
this.mode = mode;
}
void setMode(Mode mode) {
this.mode = mode;
}
String perform() {
return mode.act();
}
}
public static void main(String[] args) {
Unit u = new Unit(new AttackMode());
System.out.println(u.perform());
u.setMode(new DefendMode());
System.out.println(u.perform());
}
}
First
perform()runs while the mode isAttackMode, so it returns"attacks".setModereplaces the field with aDefendMode.Second
perform()now delegates to the new mode, returning"defends".
A class needs behaviour drawn from two different base classes. Why is inheritance a poor fit in Java?
Java permits single inheritance of classes only. To draw on two independent sources of behaviour you compose — hold an instance of each in a field — since a class may implement many interfaces and hold many collaborators, but extend just one class.
Recap
- Inheriting to borrow a method drags in the parent's whole public API, including behaviour the subclass cannot honestly perform.
- Composition means holding another object in a field and delegating the call to it — you pull in only what you chose.
- A delegated call is just ordinary dispatch on a field: read it as two hops, into your method then into the collaborator.
- Composition lets one object combine several capabilities and swap them at runtime; single inheritance can do neither.
- Use the is-a test: inherit genuine identity, compose everything you merely use.
Next you will turn these instincts into named principles — Single Responsibility, Open/Closed, and Liskov Substitution — and learn to spot the designs that violate them.