Version sans JavaScript, en lecture seule : les ateliers, les quiz et l’exécution du code n’y sont pas. Version complète · Catalogue · Se connecter
Support de cours · chapitre 4
Hériter ou composer
L'héritage est la relation la plus forte entre deux classes. Ce chapitre présente ce qu'il apporte, ce qu'il coûte, et les cas où il vaut mieux recourir à la composition.
Ce chapitre prolonge la séquence 2. On utilise l'héritage quand la sous-classe doit pouvoir remplacer la super-classe, et la composition quand il s'agit de combiner des comportements.
Ce qu'apporte l'héritage
class CanardEnPlastique {
private String nom;
private int prix; // en centimes
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); // la mère d'abord, toujours
this.accessoire = accessoire;
}
@Override public void cancaner() { System.out.println("coin (mais stylé)"); }
}
L'héritage apporte deux choses :
- la réutilisation :
CanardSwagn'a pas eu à réécrire le nom ni le prix ; - le polymorphisme : partout où l'on attend un
CanardEnPlastique, on peut utiliser unCanardSwag.
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é) ← l'objet réel décide
Ce que coûte l'héritage
Une hiérarchie de classes fige un choix fait au début du projet, c'est-à-dire au moment où l'on connaissait le moins bien le besoin.
class Canard {
void nager() { }
void voler() { }
void cancaner() { }
}
class CanardEnPlastique extends Canard {
// rien à redéfinir... pour l'instant
}
Le besoin évolue ensuite : un canard jouet ne vole pas, et il fait « pouet ».
class CanardEnPlastique extends Canard {
@Override void voler() {
throw new UnsupportedOperationException("Un jouet ne vole pas !");
}
@Override void cancaner() { System.out.println("pouet pouet"); }
}
Dans cet exemple, l'erreur ne vient pas de CanardEnPlastique mais de la décision d'avoir placé voler() dans Canard.
Un exemple du JDK : Stack extends Vector
La classe Stack du JDK est un exemple connu d'héritage mal choisi.
class Stack extends Vector {
// hérite de toutes les méthodes de Vector
}
Une pile ne devrait offrir que deux opérations, empiler et dépiler. En héritant de Vector, elle hérite aussi de insertElementAt et removeElementAt, qui permettent de modifier le milieu de la pile. C'est donc sa classe mère qui rompt l'abstraction de pile.
Avec la composition, la classe n'expose que les opérations voulues :
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 utilise un Vector mais ne l'expose pas : elle a un vecteur, alors que Stack est un vecteur.
Combiner deux comportements indépendants
Supposons qu'un canard ait un comportement de vol et un comportement de cri, chacun pouvant prendre trois valeurs.
classDiagram
direction TB
class Canard
class CanardVolantCancanant
class CanardVolantMuet
class CanardVolantPouet
class CanardNageantCancanant
class autres["... et cinq autres"]
Canard <|-- CanardVolantCancanant
Canard <|-- CanardVolantMuet
Canard <|-- CanardVolantPouet
Canard <|-- CanardNageantCancanant
Canard <|-- autresAvec l'héritage, il faut une classe par combinaison : CanardVolantCancanant, CanardVolantMuet, CanardVolantPouet, CanardNageantCancanant, etc., soit neuf classes, et vingt-sept si un troisième axe apparaît.
Avec la composition, une seule classe suffit :
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; } // ← et même en cours de route
}
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 <|.. MuetEt l'héritage multiple ?
Java interdit l'héritage multiple entre classes, pour éviter le problème du diamant : si Canard héritait de Oiseau et de Jouet, qui héritent tous deux de Objet, on ne saurait pas quelle implémentation appliquer.
Java propose à la place les interfaces : une classe peut en implémenter autant qu'elle le souhaite.
interface Volant { void voler(); }
interface Flottant { void flotter(); }
class Canard implements Volant, Flottant {
public void voler() { }
public void flotter() { }
}
Cette solution est plus souple, mais elle a ses propres dérives, par exemple une classe sans rôle clair qui implémente dix interfaces. Le chapitre 5 explique comment bien les utiliser.
Choisir entre héritage et composition
Question d'épreuve
Pourquoi préférer la composition à l'héritage pour combiner deux comportements indépendants ?
L'héritage impose une classe par combinaison (n classes pour n valeurs, n×m pour deux axes), alors que la composition combine les comportements dans une seule classe. Elle permet en outre de changer le comportement à l'exécution.