Version without JavaScript, read-only: workshops, quizzes and running code are not available here. Full version · Catalogue · Sign in

Lecture notes · chapter 4

Inherit or compose

Inheritance is the strongest relationship between two classes. This chapter presents what it brings, what it costs, and the cases where composition is the better choice.

This chapter extends sequence 2. Inheritance is used when the subclass must be able to replace the superclass, and composition when behaviours have to be combined.

What inheritance brings

class CanardEnPlastique {
    private String nom;
    private int prix;                 // in cents

    CanardEnPlastique(String nom, int prix) { this.nom = nom; this.prix = prix; }
    public void cancaner() { System.out.println("coin coin"); }
}

class CanardSwag extends CanardEnPlastique {
    private final String accessoire;

    CanardSwag(String nom, int prix, String accessoire) {
        super(nom, prix);             // the parent first, always
        this.accessoire = accessoire;
    }

    @Override public void cancaner() { System.out.println("coin (mais stylé)"); }
}

Inheritance brings two things:

  • reuse: CanardSwag did not have to rewrite the name or the price;
  • polymorphism: wherever a CanardEnPlastique is expected, a CanardSwag can be used.
List<CanardEnPlastique> bassin = new ArrayList<>();
bassin.add(new CanardEnPlastique("Gérard", 499));
bassin.add(new CanardSwag("Jacqueline", 999, "cape"));

for (CanardEnPlastique c : bassin) c.cancaner();
// coin coin
// coin (mais stylé)      ← the actual object decides

What inheritance costs

A class hierarchy freezes a choice made at the start of the project, that is, at the time when the requirements were least well understood.

class Canard {
    void nager() { }
    void voler() { }
    void cancaner() { }
}

class CanardEnPlastique extends Canard {
    // nothing to override... for now
}

The requirements then change: a toy duck does not fly, and it goes “squeak”.

class CanardEnPlastique extends Canard {
    @Override void voler() {
        throw new UnsupportedOperationException("Un jouet ne vole pas !");
    }
    @Override void cancaner() { System.out.println("pouet pouet"); }
}

In this example, the mistake is not in CanardEnPlastique but in the decision to put voler() in Canard.

An example from the JDK: Stack extends Vector

The JDK's Stack class is a well-known example of a poor choice of inheritance.

class Stack extends Vector {
    // inherits all the methods of Vector
}

A stack should only offer two operations, push and pop. By inheriting from Vector, it also inherits insertElementAt and removeElementAt, which allow the middle of the stack to be modified. It is therefore its own parent class that breaks the stack abstraction.

With composition, the class exposes only the intended operations:

class Pile<T> {
    private final Vector<T> elements = new Vector<>();

    void empiler(T item) { elements.add(item); }
    T depiler() { return elements.remove(elements.size() - 1); }
}

Pile uses a Vector but does not expose it: it has a vector, whereas Stack is a vector.

Combining two independent behaviours

Suppose a duck has a flying behaviour and a quacking behaviour, each of which can take three values.

classDiagram
    direction TB
    class Canard
    class CanardVolantCancanant
    class CanardVolantMuet
    class CanardVolantPouet
    class CanardNageantCancanant
    class autres["... and five others"]

    Canard <|-- CanardVolantCancanant
    Canard <|-- CanardVolantMuet
    Canard <|-- CanardVolantPouet
    Canard <|-- CanardNageantCancanant
    Canard <|-- autres

With inheritance, you need one class per combination: CanardVolantCancanant, CanardVolantMuet, CanardVolantPouet, CanardNageantCancanant, and so on, which makes nine classes, and twenty-seven if a third axis appears.

With composition, a single class is enough:

interface VolStrategy  { void voler(); }
interface ChantStrategy { void chanter(); }

class Canard {
    private VolStrategy vol;
    private ChantStrategy chant;

    Canard(VolStrategy vol, ChantStrategy chant) { this.vol = vol; this.chant = chant; }

    void effectuerVol()   { vol.voler(); }
    void effectuerChant() { chant.chanter(); }

    void equiper(VolStrategy v) { this.vol = v; }   // ← and even along the way
}
classDiagram
    direction LR
    class Canard {
        -VolStrategy vol
        -ChantStrategy chant
        +effectuerVol()
        +effectuerChant()
        +equiper(VolStrategy)
    }
    class VolStrategy {
        <<interface>>
        +voler()
    }
    class ChantStrategy {
        <<interface>>
        +chanter()
    }
    Canard o-- VolStrategy
    Canard o-- ChantStrategy
    VolStrategy <|.. VolerAvecAiles
    VolStrategy <|.. NePasVoler
    ChantStrategy <|.. Cancaner
    ChantStrategy <|.. Muet

And multiple inheritance?

Java forbids multiple inheritance between classes, to avoid the diamond problem: if Canard inherited from Oiseau and from Jouet, which both inherit from Objet, there would be no way to know which implementation to apply.

Instead, Java offers interfaces: a class can implement as many as it needs.

interface Volant   { void voler(); }
interface Flottant { void flotter(); }

class Canard implements Volant, Flottant {
    public void voler()   { }
    public void flotter() { }
}

This solution is more flexible, but it can be misused too, for example with a class that has no clear role and implements ten interfaces. Chapter 5 explains how to use them well.

Choosing between inheritance and composition

Exam question

Why prefer composition to inheritance to combine two independent behaviours?

Inheritance imposes one class per combination (n classes for n values, n×m for two axes), whereas composition combines the behaviours in a single class. It also allows the behaviour to be changed at run time.