cursus.

Cours 3 · Héritage et polymorphismeLeçon 1 sur 2

Héritage

6 h de lecture8 sections Version PDF

À la fin de cette leçon, vous saurez

La relation « est-un », extends et super, redéfinition de méthodes, hiérarchie de classes, et le choix entre héritage et composition.

Chaque année, le même code apparaît en TP :

class Moteur  { int puissance; void demarrer() { ... } }class Voiture extends Moteur { String marque; }

Le raisonnement est compréhensible : une voiture a une puissance, une voiture démarre, et Moteur possède déjà les deux. Hériter évite de réécrire.

Le résultat est pourtant une faute de conception, et elle se voit à une conséquence : partout où le programme attend un Moteur, on pourra désormais lui donner une Voiture. Un atelier de réparation de moteurs acceptera une voiture entière. Une liste de moteurs en stock contiendra des véhicules.

Ce chapitre donne le mécanisme de l'héritage, puis le critère qui dit quand on a le droit de s'en servir. Le second compte plus que le premier — c'est le deuxième point qui coince du semestre.

Le mécanisme

public class Document {    protected String titre;    private int identifiant;     public Document(String titre) { this.titre = titre; }    public String decrire() { return "Document : " + titre; }} public class Livre extends Document {    private String auteur;     public Livre(String titre, String auteur) {        super(titre);                 /* appelle le constructeur du parent */        this.auteur = auteur;    }     @Override    public String decrire() {        return super.decrire() + ", par " + auteur;    }}

Quatre points à relever.

extends établit la relation. Livre est un Document : il hérite de tous les membres non privés de son parent — attributs et méthodes — et peut en ajouter.

Les attributs privés du parent existent mais sont inaccessibles. Chaque Livre contient bien un identifiant en mémoire ; simplement, le code de Livre ne peut pas le lire directement. Il doit passer par un accesseur du parent, s'il en existe un. C'est l'encapsulation du chapitre 3, qui s'applique aussi aux filles — et c'est ce qui justifie l'existence de protected.

super(...) appelle le constructeur du parent, et doit être la première instruction. La raison est de bon sens : la partie parente de l'objet doit être valide avant qu'on ne complète la partie fille. Si on l'omet, Java insère un super() sans argument — et si le parent n'a pas de constructeur sans argument, le code ne compile pas. C'est une erreur fréquente et son message est déroutant.

@Override n'est pas décoratif. Cette annotation demande au compilateur de vérifier qu'une méthode du parent est bien redéfinie. Sans elle, une faute de frappe — decrir() au lieu de decrire() — crée une nouvelle méthode qui ne sera jamais appelée à la place de l'autre, et tout compile. Il faut la mettre systématiquement.

Redéfinir, et prolonger

Redéfinir (override), c'est fournir dans la fille une méthode de même signature que dans le parent. C'est elle qui sera exécutée sur un objet de la fille — le chapitre 6 dira précisément pourquoi.

super.decrire() appelle la version du parent depuis la fille. C'est ce qui permet de prolonger un comportement au lieu de le remplacer : faire ce que fait le parent, puis ajouter. L'oubli de cet appel est une source classique de bogues — une fille qui redéfinit initialiser() sans appeler celle du parent laisse la moitié de l'objet non initialisée.

Trois interdits à connaître. On ne peut pas restreindre la visibilité en redéfinissant : une méthode public ne peut pas devenir protected chez la fille, sinon un objet fille vu comme un parent perdrait une méthode qu'il est censé avoir. On ne peut pas redéfinir une méthode final. Et une méthode static n'est pas redéfinie mais masquée, ce qui est un piège dont le chapitre 6 montrera l'effet.

Une hiérarchie d'héritage se lit d'un coup d'œil sous forme de graphe. Dans le diagramme interactif ci-dessous, chaque flèche part d'une sous-classe et pointe vers son parent (extends) : Livre, Magazine et CD sont des Document et héritent de son titre et de sa méthode afficher(). Déplacez les nœuds pour explorer la structure.

Diagramme · Une hiérarchie d'héritage (extends)

  • Document — attributs : - titre : String — méthodes : + afficher() : void
  • Livre — attributs : - auteur : String
  • Magazine — attributs : - numero : int
  • CD — attributs : - duree : int

Relations

  • Livre ▷ hérite Document
  • Magazine ▷ hérite Document
  • CD ▷ hérite Document

Le test « est-un »

Voici le vrai sujet du chapitre.

Avant d'écrire extends, on formule la phrase à voix haute : « Un B est-il un A ? »

Un Livre est un Document.          ✔  cohérentUn Rectangle est une Forme.        ✔Un Étudiant est une Personne.      ✔Une Voiture est un Moteur.         ✘  non — elle en POSSÈDE unUn Compte est une Banque.          ✘  non — il appartient à une banqueUne Commande est une ListeArticles ✘  non — elle en contient une

Le test échoue le plus souvent de la même façon : on hérite parce qu'on a repéré des attributs communs, ce qui est un argument de réutilisation, pas de nature.

Il faut aller un cran plus loin, car « est-un » reste parfois trompeur. Le critère rigoureux est le suivant : tout ce qui est vrai d'un objet de la classe mère doit rester vrai d'un objet de la classe fille. Autrement dit, un code écrit pour le parent doit continuer de fonctionner correctement si on lui donne une fille — sans le savoir.

Le contre-exemple d'école est celui du carré et du rectangle. Un carré est un rectangle, en mathématiques. Et pourtant :

class Rectangle { void setLargeur(double l); void setHauteur(double h); double aire(); }class Carre extends Rectangle { /* setLargeur modifie aussi la hauteur */ }

Un code écrit pour Rectangle peut légitimement faire : fixer la largeur à 5, fixer la hauteur à 4, et attendre une aire de 20. Sur un Carre, il obtient 16. Le code du parent est cassé par la fille, alors même que la phrase « est-un » était vraie.

La leçon est double. La relation « est-un » porte sur le COMPORTEMENT, pas sur le vocabulaire. Et quand un héritage force à écrire des exceptions dans le parent — « sauf si c'est un carré » — c'est le signe qu'il faut le défaire.

Quiz · vérifiez votre compréhension Sans réponse

Un étudiant écrit class Voiture extends Moteur parce que les deux ont une puissance et une méthode demarrer(). Quelle est la conséquence concrète ?

Héritage contre composition

Quand « est-un » échoue, la bonne relation est presque toujours « a-un » — la composition.

public class Voiture {    private final Moteur moteur;          /* une Voiture A UN moteur */    private String marque;     public Voiture(String marque, Moteur moteur) {        this.marque = marque;        this.moteur = moteur;    }     public void demarrer() {        moteur.demarrer();                /* délégation */    }}

Le code de Moteur n'est toujours pas dupliqué : il est appelé, au lieu d'être hérité. Et la relation absurde disparaît — aucune Voiture ne peut plus se faire passer pour un Moteur.

Quatre avantages, qui expliquent la recommandation générale de préférer la composition.

On n'expose que ce qu'on veut. L'héritage rend publiques toutes les méthodes publiques du parent, y compris celles qui n'ont aucun sens pour la fille. La composition ne délègue que ce qu'on choisit.

On peut changer de composant à l'exécution — remplacer le moteur — ce qu'un lien d'héritage, fixé à la compilation, ne permet pas.

On peut en avoir plusieurs. Une Voiture a quatre Roue ; l'héritage ne saurait pas l'exprimer.

Le couplage est plus faible. Une fille dépend des détails internes de son parent : une modification du parent peut casser toutes ses filles, sans qu'aucune signature n'ait changé. C'est le « problème de la classe de base fragile », et il n'a pas d'équivalent en composition.

La règle de conduite, à retenir telle quelle : héritage quand la relation est vraiment « est-un » et que la substituabilité tient ; composition dans tous les autres cas — c'est-à-dire le plus souvent.

Quiz · vérifiez votre compréhension Sans réponse

Un Carre hérite de Rectangle et redéfinit setLargeur pour modifier aussi la hauteur. Un code écrit pour Rectangle fixe la largeur à 5, la hauteur à 4, et attend une aire de 20. Que conclure ?

À vous

L'exercice attaque les deux difficultés du chapitre dans l'ordre.

D'abord un jeu de paires candidates — Livre/Document, Voiture/Moteur, Carre/Rectangle et quelques autres — sur lesquelles vous appliquez le test « est-un » et justifiez votre verdict. Le corrigé donne le raisonnement, pas seulement la réponse.

Ensuite la démonstration mécanique de la rupture : un test écrit pour Rectangle — fixer deux dimensions, vérifier l'aire — que vous exécutez sur un Rectangle puis sur un Carre. Le même test passe puis échoue, sans avoir été modifié. C'est la définition opérationnelle de la substituabilité, et elle vaut mieux qu'un long discours.

Enfin la refonte de Voiture extends Moteur en composition, avec délégation — et la vérification qu'une Voiture ne peut plus être rangée dans une liste de moteurs.

Exercice · JavaScript · à vous de jouer

Appliquez le test « est-un », mesurez la rupture de substituabilité, puis remplacez un héritage fautif par une délégation.

En attente
// ── 1. Le test « est-un » ─────────────────────────────────────────────────
const PAIRES = [
  { fille: "Livre",    mere: "Document",      indice: "un livre est un document" },
  { fille: "Voiture",  mere: "Moteur",        indice: "une voiture démarre, et a une puissance" },
  { fille: "Etudiant", mere: "Personne",      indice: "un étudiant a un nom et un âge" },
  { fille: "Compte",   mere: "Banque",        indice: "un compte appartient à une banque" },
  { fille: "Carre",    mere: "Rectangle",     indice: "en mathématiques, c'en est un" },
  { fille: "Pile",     mere: "ArrayList",     indice: "une pile stocke des éléments en liste" },
];

// ← à écrire : pour chaque paire, répondre estUn (true/false) et dire
//   pourquoi. Deux questions : la phrase « une F est une M » est-elle vraie
//   de NATURE ? et un code écrit pour M fonctionnerait-il sur une F ?
const VERDICTS = {};

// ── 2. La substituabilité, mesurée ────────────────────────────────────────
class Rectangle {
  constructor(l, h) { this._largeur = l; this._hauteur = h; }
  setLargeur(l) { this._largeur = l; }
  setHauteur(h) { this._hauteur = h; }
  aire() { return this._largeur * this._hauteur; }
}

class Carre extends Rectangle {
  constructor(cote) { super(cote, cote); }
  setLargeur(l) { this._largeur = l; this._hauteur = l; }   // maintient l'invariant
  setHauteur(h) { this._largeur = h; this._hauteur = h; }
}

// Un test écrit POUR Rectangle, par quelqu'un qui ignore que Carre existe.
function testEcritPourRectangle(forme) {
  forme.setLargeur(5);
  forme.setHauteur(4);
  return { aire: forme.aire(), attendu: 20 };
}

// ── 3. Voiture : héritage fautif, puis composition ────────────────────────
class Moteur {
  constructor(puissance) { this.puissance = puissance; this.tourne = false; }
  demarrer() { this.tourne = true; return "moteur " + this.puissance + " ch démarré"; }
  arreter() { this.tourne = false; }
}

class VoitureHeritee extends Moteur {          // ← la faute
  constructor(marque, puissance) { super(puissance); this.marque = marque; }
}

class Voiture {
  #moteur;
  constructor(marque, moteur) { this.marque = marque; this.#moteur = moteur; }
  // ← à écrire : déléguer demarrer() au moteur, sans hériter de lui
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Remplissez VERDICTS pour les six paires.
// 2. Lancez testEcritPourRectangle sur un Rectangle puis sur un Carre.
// 3. Écrivez la délégation dans Voiture, et vérifiez qu'une Voiture ne peut
//    plus être rangée dans une liste de moteurs.

console.log("   " + testEcritPourRectangle(new Rectangle(1, 1)).aire);

Console de sortie
Le résultat s'affiche dans la console

En travaux pratiques

Travaux pratiques 5 · sur machine

Le deuxième point qui coince : hériter à tort

Écrire une hiérarchie qui paraît naturelle, prouver qu'elle est fausse, et la remplacer par de la composition — pour que le critère de choix devienne un réflexe.

4 h
Avant de commencer
  • Les TP 1 à 4
  1. 1. Une hiérarchie légitime

    Faites dériver Livre, Dvd et Revue de Document. Mettez dans la classe mère ce qui est commun, et rien d'autre. Comptez les lignes économisées.

  2. 2. super

    Écrivez les constructeurs des filles en appelant celui de la mère. Retirez l'appel et lisez l'erreur. Expliquez pourquoi il doit être en première instruction.

  3. 3. La hiérarchie qui semble juste

    Écrivez une classe Rectangle avec largeur, hauteur et surface. Faites-en dériver Carre, en redéfinissant les modificateurs pour garder les côtés égaux.

  4. 4. La casser

    Écrivez une méthode qui prend un Rectangle, lui met une largeur de 5 et une hauteur de 4, et vérifie que la surface vaut 20. Passez-lui un Carre.

  5. 5. Le principe

    Énoncez en une phrase la règle que le carré viole, et vérifiez si votre hiérarchie de documents la respecte.

  6. 6. Le deuxième contre-exemple

    Faites dériver une Pile d'une classe de liste. Ajoutez un élément par la méthode d'insertion de la liste et constatez ce que devient l'invariant de la pile.

  7. 7. Composer

    Réécrivez la pile en CONTENANT une liste plutôt qu'en héritant d'elle. Comparez la surface publique des deux versions.

  8. 8. Le critère

    Écrivez la question à se poser avant tout héritage, et appliquez-la aux cinq relations suivantes : Livre/Document, Carre/Rectangle, Pile/Liste, Cercle/Forme, Employe/Personne.

C'est réussi quand
  • Votre test échoue avec un Carre, sans que le test soit fautif
  • Vous énoncez le principe de substitution sans le relire
  • Votre pile par composition n'expose aucune méthode qui casse son invariant

Ce que la suite en fait

Le chapitre 6 exploite ce que celui-ci a construit. Puisqu'un Livre est un Document, une variable déclarée Document peut désigner un Livre — et la question devient : quelle version de decrire() s'exécute alors ? C'est le polymorphisme, et c'est l'obstacle de l'année.

Le contre-exemple du carré y trouvera aussi sa vraie sortie : une interface commune, qui ne promet que ce que les deux formes savent réellement tenir.

À retenir

Flashcards · 1 / 5Toucher pour retourner
Fin de la leçon

Vous avez parcouru les 8 sections.

Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.