Why this matters
- Inheritance is the most over-used feature in object-oriented code, and the resulting hierarchies are the hardest thing to refactor later.
- "Favour composition over inheritance" is advice everyone repeats and few can justify. The justification is specific and worth knowing.
- Java's single-inheritance rule forces a design decision early, so understanding the trade-off is not optional.
How inheritance works
public class Vehicle {
protected final String registration;
private int odometer;
public Vehicle(String registration) {
this.registration = registration;
}
public void drive(int kilometres) {
odometer += kilometres;
}
public String describe() {
return "Vehicle " + registration;
}
}
public class Car extends Vehicle {
private final int seats;
public Car(String registration, int seats) {
super(registration); // must come first
this.seats = seats;
}
@Override
public String describe() {
return super.describe() + " with " + seats + " seats";
}
}
What a subclass gets, and what it does not
- It inherits all
publicandprotectedmembers, plus package-private ones if it is in the same package. - It does not inherit
privatemembers. They exist in the object but are unreachable by name. - It does not inherit constructors. It must call one of the parent's via
super(...). super.method()calls the parent's version, which is how you extend behaviour rather than replace it.- Every class without an explicit
extendssilently extendsObject.
Single inheritance, and the reason for it
A Java class may extend exactly one class but implement any number of interfaces.
class Hybrid extends Car implements Electric, Serviceable { } // legal
// class Broken extends Car, Truck { } // not legal
The reason is the diamond problem. If a class inherited concrete state and behaviour from two
parents that both defined start(), there would be no principled way to decide which runs.
Interfaces avoid this because, historically, they carried no implementation at all.
Composition
Composition means holding another object as a field and delegating to it.
public class Engine {
void start() { }
void stop() { }
}
public class Car {
private final Engine engine = new Engine(); // has-a
public void startUp() {
engine.start(); // delegation
}
}
The difference from inheritance is that Car chooses exactly which parts of Engine to expose.
It is not forced to inherit the whole surface.
Why composition usually wins
| Aspect | Inheritance | Composition |
|---|---|---|
| Relationship | is-a | has-a |
| Coupling | Tight — subclass depends on parent internals | Loose — only on the part's public API |
| Fixed at | Compile time | Run time, by swapping the field |
| How many | One superclass | As many parts as you like |
| Exposed surface | Everything public in the parent | Only what you choose to delegate |
| Parent changes | May silently break subclasses | Contained behind the delegation |
Relationship
Inheritanceis-aCompositionhas-aCoupling
InheritanceTight — subclass depends on parent internalsCompositionLoose — only on the part's public APIFixed at
InheritanceCompile timeCompositionRun time, by swapping the fieldHow many
InheritanceOne superclassCompositionAs many parts as you likeExposed surface
InheritanceEverything public in the parentCompositionOnly what you choose to delegateParent changes
InheritanceMay silently break subclassesCompositionContained behind the delegation
Composition trades a little extra typing for the ability to change your mind later.
The deepest problem with inheritance is the fragile base class. A subclass can depend on how the parent is implemented, not just what it promises, and an innocent-looking change to the parent then breaks it.
// A counting set, by inheritance — and it is wrong
public class CountingSet<E> extends HashSet<E> {
private int added;
@Override
public boolean add(E element) {
added++;
return super.add(element);
}
@Override
public boolean addAll(Collection<? extends E> elements) {
added += elements.size();
return super.addAll(elements); // which internally calls add() for each
}
public int added() { return added; }
}
CountingSet<String> set = new CountingSet<>();
set.addAll(List.of("a", "b", "c"));
System.out.println(set.added()); // 6, not 3
HashSet.addAll is implemented by calling add repeatedly. The subclass counted each element
twice, once in its own addAll and once in its own add. Nothing in the documented contract told
you that, and a future JDK could change it either way.
The composed version has no such problem:
public class CountingSet<E> {
private final Set<E> delegate = new HashSet<>();
private int added;
public boolean add(E element) {
added++;
return delegate.add(element);
}
public boolean addAll(Collection<? extends E> elements) {
added += elements.size();
return delegate.addAll(elements); // delegate's internals cannot call back in
}
public int added() { return added; }
}
When inheritance is the right answer
It is not always wrong. Reach for it when all of these hold.
- The is-a relationship is genuinely true, not merely convenient for reuse.
- The subclass can be used anywhere the parent is expected without surprising anyone — the substitution principle.
- The parent was designed for extension, with documented extension points and a stable contract.
- You control both classes, or the parent explicitly supports subclassing.
this and super
thisrefers to the current object.this.field = fielddisambiguates a field from a parameter of the same name.this(...)calls another constructor in the same class and must be the first statement.superaccesses the parent's members, which is the only way to reach an overridden method.super(...)calls a parent constructor, must be first, and is inserted automatically assuper()if you omit it.
Common misreadings
- "Inheritance means less code, so it is better." It means less typing now and more coupling forever. Count the cost on both sides.
- "
protectedfields are good for subclasses." They make the field part of your public contract for anyone who extends you. Preferprotectedmethods overprotectedfields. - "Java has no multiple inheritance." It has no multiple inheritance of state. Multiple inheritance of type, and since Java 8 of default behaviour, both exist.
- "A subclass inherits private fields." The memory is there, but the names are not accessible. Only a parent method can reach them.
- "Deep hierarchies show good design." Depth beyond two or three levels is usually a sign that composition was the right tool.
Quick recall
Everything you need if you only revisit this box.
- is-a → inheritance. has-a → composition. When unsure, compose.
- A subclass inherits accessible members but never constructors, and
super(...)must be the first statement. - Java allows one superclass, many interfaces, avoiding the diamond problem for state.
- Inheritance is compile-time and tight; composition is run-time and loose, and exposes only what you delegate.
- The fragile base class problem: subclasses can depend on parent internals, as the
CountingSetdouble-count shows. - Use inheritance only when is-a is true, substitution holds, and the parent was designed for extension. Otherwise mark the class
final.
Test yourself
Answer these before moving on — recall is what makes it stick.