Programmation orientée objet · C3 Héritage et polymorphisme · Chapitre 1 · 6 h
Héritage
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 uneLe 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 · 1 question
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 ?
- Aucune : c'est une factorisation valide, puisque le code commun n'est écrit qu'une fois — factorisation valide
- Partout où le programme attend un Moteur, on pourra désormais lui donner une Voiture : un atelier de réparation de moteurs acceptera un véhicule entier, et une liste de moteurs contiendra des voitures — substitution absurde
- Le code ne compilera pas, car Moteur et Voiture n'ont pas de constructeur compatible — erreur de compilation
Réponse : L'héritage ne factorise pas seulement du code : il DÉCLARE une relation de nature, et cette déclaration a une conséquence opérationnelle immédiate — la substituabilité. Écrire extends, c'est affirmer qu'une Voiture peut être utilisée partout où un Moteur est attendu, ce qui est absurde et produira des signatures de méthodes trompeuses, des collections hétéroclites et des tests impossibles à écrire. Le raisonnement de l'étudiant repose sur des ATTRIBUTS COMMUNS, ce qui est un argument de réutilisation et jamais un argument de nature. La bonne relation est « a-un » : la Voiture POSSÈDE un Moteur, donc un attribut privé de type Moteur, et elle délègue demarrer() en appelant moteur.demarrer(). Le code commun n'est pas dupliqué pour autant — il reste dans Moteur.
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 · 1 question
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 ?
- Rien d'anormal : un carré a bien une largeur égale à sa hauteur, le code appelant est simplement mal écrit — appelant mal écrit
- L'héritage est fautif malgré la phrase « un carré est un rectangle » : un code correct pour le parent est cassé par la fille, donc la substituabilité ne tient pas — le critère porte sur le COMPORTEMENT, pas sur le vocabulaire — substituabilité rompue
- Il suffit de rendre setLargeur final dans Rectangle pour interdire la redéfinition — final suffit
Réponse : C'est le contre-exemple d'école, et il montre que « est-un » énoncé en français ne suffit pas. Le critère rigoureux est : tout ce qui est vrai d'un objet de la classe mère doit rester vrai d'un objet de la classe fille, de sorte qu'un code écrit pour le parent continue de fonctionner s'il reçoit une fille SANS LE SAVOIR. Ici l'appelant ne fait rien d'illégitime : Rectangle promet deux dimensions indépendantes, il en use. C'est Carre qui rompt la promesse. Rendre setLargeur final déplacerait le problème sans le résoudre — Carre ne pourrait plus maintenir son invariant et deviendrait un rectangle ordinaire. La sortie est de ne pas hériter : soit Carre est une classe indépendante, soit les deux implémentent une interface Forme commune qui ne promet que aire(), sans setters. Signal général : quand un héritage force à écrire « sauf si c'est un… » dans le parent, il faut le défaire.
À 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 de code
Appliquez le test « est-un », mesurez la rupture de substituabilité, puis remplacez un héritage fautif par une délégation.
Point de départ
// ── 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);
Solution
const VERDICTS = {
Livre: { estUn: true, pourquoi: "un livre EST un document : tout ce qu'on fait d'un document (décrire, ranger, référencer) a un sens sur un livre" },
Voiture: { estUn: false, pourquoi: "elle POSSÈDE un moteur — attributs communs n'est pas nature commune. Composition" },
Etudiant: { estUn: true, pourquoi: "un étudiant EST une personne, et tout code écrit pour Personne reste correct" },
Compte: { estUn: false, pourquoi: "il APPARTIENT à une banque : c'est une association, pas une nature" },
Carre: { estUn: false, pourquoi: "vrai en mathématiques, faux en programmation : Rectangle promet deux dimensions INDÉPENDANTES, promesse qu'un carré ne peut pas tenir" },
Pile: { estUn: false, pourquoi: "une pile UTILISE une liste pour stocker, mais hériter exposerait get(i) et remove(i), qui violent le LIFO. Composition" },
};
console.log("— 1. le test « est-un » —");
for (const [nom, v] of Object.entries(VERDICTS)) {
console.log(" " + nom.padEnd(10) + (v.estUn ? "HÉRITAGE " : "composition") + " " + v.pourquoi);
}
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; }
get nom() { return this.constructor.name; }
}
class Carre extends Rectangle {
constructor(cote) { super(cote, cote); }
setLargeur(l) { this._largeur = l; this._hauteur = l; }
setHauteur(h) { this._largeur = h; this._hauteur = h; }
}
function testEcritPourRectangle(forme) {
forme.setLargeur(5);
forme.setHauteur(4);
return { nom: forme.nom, aire: forme.aire(), attendu: 20 };
}
console.log("");
console.log("— 2. le MÊME test, deux objets —");
for (const forme of [new Rectangle(1, 1), new Carre(1)]) {
const r = testEcritPourRectangle(forme);
console.log(" " + r.nom.padEnd(10) + " aire = " + String(r.aire).padStart(3) +
" attendu " + r.attendu + (r.aire === r.attendu ? " ok" : " ÉCHEC"));
}
console.log(" Le test n'a pas été modifié d'un caractère. L'appelant ne fait rien");
console.log(" d'illégitime : Rectangle promet deux dimensions indépendantes, il en");
console.log(" use. C'est Carre qui rompt la promesse — donc l'héritage est fautif,");
console.log(" malgré la phrase « un carré est un rectangle ».");
console.log("");
console.log("— 3. héritage fautif contre 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 {
constructor(marque, puissance) { super(puissance); this.marque = marque; }
}
class Voiture {
#moteur;
constructor(marque, moteur) { this.marque = marque; this.#moteur = moteur; }
// DÉLÉGATION : on appelle le moteur au lieu d'hériter de lui. Le code de
// Moteur n'est pas dupliqué, et on n'expose que ce qu'on a choisi.
demarrer() { return this.marque + " : " + this.#moteur.demarrer(); }
get puissance() { return this.#moteur.puissance; }
changerMoteur(m) { this.#moteur = m; } // impossible avec l'héritage
}
// Un atelier qui ne travaille que sur des moteurs.
function atelier(moteurs) {
return moteurs.map((m) => (m instanceof Moteur ? "révisé" : "REFUSÉ : ce n'est pas un moteur"));
}
const stock = [new Moteur(90), new VoitureHeritee("Renault", 110)];
console.log(" avec l'héritage fautif : " + atelier(stock).join(" | "));
console.log(" Une voiture entière est passée pour un moteur, et l'atelier l'a prise.");
const stock2 = [new Moteur(90), new Voiture("Renault", new Moteur(110))];
console.log(" avec la composition : " + atelier(stock2).join(" | "));
console.log(" " + new Voiture("Peugeot", new Moteur(75)).demarrer());
console.log(" La relation absurde a disparu, et l'on peut même changer de moteur");
console.log(" à l'exécution — ce qu'un lien d'héritage, fixé à la compilation,");
console.log(" n'aurait jamais permis.");
En travaux pratiques
Travaux pratiques 5 · 4 h
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.
Avant de commencer
- Les TP 1 à 4
Énoncé
- 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.
- 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.
- 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.
- 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. Indice : La méthode ne fait rien d'illégitime : elle utilise le contrat de Rectangle.
- Le principe — Énoncez en une phrase la règle que le carré viole, et vérifiez si votre hiérarchie de documents la respecte.
- 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.
- Composer — Réécrivez la pile en CONTENANT une liste plutôt qu'en héritant d'elle. Comparez la surface publique des deux versions.
- 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
Correction
public abstract class Document {
protected final String titre;
protected boolean disponible = true;
protected Document(String titre) { this.titre = titre; }
public boolean emprunter() { … }
public abstract int dureeEmpruntEnJours(); /* diffère par type */
}
public class Livre extends Document {
private final int pages;
public Livre(String titre, int pages) {
super(titre); /* PREMIÈRE instruction */
this.pages = pages;
}
@Override public int dureeEmpruntEnJours() { return 21; }
}super(...) doit être en première instruction parce que la partie héritée doit être construite avant qu'on puisse s'appuyer dessus : sinon une méthode de la mère pourrait lire des champs non initialisés. Si vous ne l'écrivez pas, Java insère super() sans argument — et l'erreur apparaît quand la mère n'a pas de constructeur sans argument.
class Carre extends Rectangle {
@Override public void setLargeur(int l) { super.setLargeur(l);
super.setHauteur(l); }
@Override public void setHauteur(int h) { super.setLargeur(h);
super.setHauteur(h); }
}
void tester(Rectangle r) {
r.setLargeur(5);
r.setHauteur(4);
assert r.surface() == 20; /* légitime pour un Rectangle */
}
tester(new Rectangle()) → 20, réussit
tester(new Carre()) → 16, ÉCHOUELa méthode tester est irréprochable : elle n'utilise que le contrat public de Rectangle. C'est la sous-classe qui a menti. En mathématiques un carré EST un rectangle ; en programmation, ce qui compte n'est pas la nature des objets mais leur COMPORTEMENT — et un rectangle mutable dont les côtés varient indépendamment n'est pas substituable par un carré.
Partout où l'on attend une classe mère, on doit pouvoir mettre n'importe quelle fille SANS que le programme cesse d'être correct. en pratique, une sous-classe ne doit jamais : - renforcer ce qu'elle exige en entrée - affaiblir ce qu'elle garantit en sortie - casser un invariant de la mère - lever une exception que la mère ne déclarait pas
Ce principe transforme une question de goût en question vérifiable : écrivez les tests de la classe mère, et faites-les passer à chaque fille. S'ils échouent, l'héritage est faux — et ce test mécanique vaut mieux que n'importe quelle discussion sur la modélisation. À noter : si Rectangle était IMMUABLE, sans modificateurs, Carre pourrait légitimement en dériver.
class Pile<T> extends ArrayList<T> {
public void empiler(T t) { add(t); }
public T depiler() { return remove(size() - 1); }
}
Pile<String> p = new Pile<>();
p.empiler("a"); p.empiler("b");
p.add(0, "triché"); /* HÉRITÉ d'ArrayList : insertion au milieu */
p.remove(0); /* HÉRITÉ : retrait par le bas */
/* la pile n'est plus une pile : l'invariant DERNIER ENTRÉ,
PREMIER SORTI est cassé, et on ne pouvait pas l'empêcher */Hériter, c'est hériter de TOUT — y compris des méthodes qui contredisent votre invariant. Java lui-même porte la cicatrice de cette erreur : java.util.Stack hérite de Vector et souffre exactement de ce défaut, ce que la documentation officielle reconnaît en recommandant Deque à la place.
class Pile<T> {
private final List<T> elements = new ArrayList<>(); /* CONTIENT */
public void empiler(T t) { elements.add(t); }
public T depiler() {
if (elements.isEmpty()) throw new NoSuchElementException();
return elements.remove(elements.size() - 1);
}
public boolean estVide() { return elements.isEmpty(); }
}
surface publique : héritage 40+ méthodes | composition 3 méthodesOn choisit ce que l'on expose, au lieu de subir ce que l'on reçoit. On peut aussi changer ArrayList pour LinkedList sans que le moindre appelant s'en aperçoive — ce qu'un héritage rendait impossible, puisque le type de la liste faisait partie du contrat public.
La question n'est PAS « est-ce un » mais :
« puis-je le substituer partout, sans casser le contrat ? »
Livre / Document OUI — un livre fait tout ce qu'un document fait
Carre / Rectangle NON — sur un rectangle MUTABLE
Pile / Liste NON — la liste offre trop
Cercle / Forme OUI — si Forme n'expose que des opérations communes
Employe / Personne ATTENTION — souvent un RÔLE, pas une nature :
une personne peut devenir employée, cesser de l'être,
ou l'être deux fois. La composition est préférable.
par défaut : COMPOSER. Hériter seulement quand la substitution
est vérifiée par des tests.Le dernier cas est le plus instructif : l'héritage fige à la construction ce qu'un rôle a de temporaire. Un objet ne peut pas changer de classe, alors qu'une personne change d'emploi. La composition modélise cela sans effort — et c'est pourquoi la règle par défaut, dans le code industriel, est de composer.
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 · 5 cartes
- Que fait exactement extends, et qu'advient-il des attributs privés du parent ?
- extends déclare que la fille EST UN parent : elle hérite de tous ses membres non privés et peut en ajouter. Les attributs PRIVÉS du parent existent bien en mémoire dans chaque objet fille, mais le code de la fille ne peut pas les lire directement — c'est l'encapsulation, qui s'applique aussi aux filles, et c'est ce qui justifie protected.
- Quelles sont les règles de super(...) et de @Override ?
- super(...) appelle le constructeur du parent et doit être la PREMIÈRE instruction : la partie parente doit être valide avant qu'on complète la partie fille. Omis, Java insère un super() sans argument — et si le parent n'en a pas, le code ne compile pas, avec un message déroutant. @Override n'est pas décoratif : il demande au compilateur de VÉRIFIER qu'une méthode du parent est bien redéfinie ; sans lui, une faute de frappe crée une méthode nouvelle qui ne sera jamais appelée, et tout compile.
- Quel est le critère rigoureux qui autorise un héritage ?
- Pas la phrase « est-un » en français, mais la SUBSTITUABILITÉ : tout ce qui est vrai d'un objet de la classe mère doit rester vrai d'un objet de la classe fille, de sorte qu'un code écrit pour le parent continue de fonctionner s'il reçoit une fille sans le savoir. Le contre-exemple du carré et du rectangle le montre : la phrase est vraie, le comportement ne suit pas. Signal d'alarme : quand un héritage force à écrire « sauf si c'est un… » dans le parent, il faut le défaire.
- Pourquoi hériter parce que deux classes ont des attributs communs est-il une faute ?
- Parce que les attributs communs sont un argument de RÉUTILISATION, jamais un argument de NATURE. Écrire extends déclare une substituabilité : partout où un Moteur est attendu, une Voiture pourra être donnée — un atelier de moteurs acceptera un véhicule. La bonne relation est « a-un » : la Voiture POSSÈDE un moteur en attribut privé et délègue demarrer(). Le code n'est pas dupliqué pour autant : il est appelé au lieu d'être hérité.
- Quels sont les quatre avantages de la composition sur l'héritage ?
- 1) ON N'EXPOSE QUE CE QU'ON VEUT : l'héritage rend publiques toutes les méthodes du parent, la composition ne délègue que le choisi. 2) ON PEUT CHANGER DE COMPOSANT à l'exécution, un lien d'héritage étant fixé à la compilation. 3) ON PEUT EN AVOIR PLUSIEURS (quatre roues). 4) LE COUPLAGE EST PLUS FAIBLE : une fille dépend des détails internes de son parent, dont une modification peut la casser sans qu'aucune signature ait changé — c'est le problème de la classe de base fragile. Règle : héritage si « est-un » ET substituabilité, composition sinon, c'est-à-dire le plus souvent.