Ch. 5-6 — Héritage et mécanismes fins
extends, super et protected pour factoriser l'état et le comportement, type réel contre type effectif, lookup dynamique et masquage d'attributs — jusqu'aux classes abstraites.
Pourquoi hériter ?
La redondance reste « l'ennemi numéro un du développeur ». Trois classes de comptes
bancaires — StandardAccount, LimitedAccount, BonusAccount — dupliquent le même
solde, le même numéro, les mêmes credit/withdraw : écrire le même code dans plusieurs
classes n'est pas maintenable. extends définit une classe par rapport à une autre, en
ne spécifiant que les changements : la sous-classe récupère tous les attributs et
comportements accessibles de la mère.
public class BonusAccount extends StandardAccount {
private double rate;
public BonusAccount(Person p, String number, double rate) {
super(p, number); // réutilise l'initialisation du parent
this.rate = rate;
}
public void interestPayment() { super.credit(this.getBalance() * this.rate); }
}Le niveau d'accès décide de ce qui passe : balance étant private, seul
getBalance() y donne accès depuis la sous-classe ; number public reste manipulable
directement. Java impose l'héritage simple : une classe ne dérive que d'une seule
autre.
protected et la table des visibilités
Le modificateur protected (symbole UML #) ouvre un membre aux sous-classes, y
comprises celles d'un autre paquetage. Le tableau comparatif du cours se retient ainsi :
private ne sort jamais de la classe ; default (paquetage) s'arrête aux classes du
même paquet ; protected franchit les paquetages mais seulement par héritage ;
public est universel.
Type réel, type effectif
- le type réel dépend de la classe ayant servi à la création de l'instance : il est figé ;
- le type effectif est celui de la référence : il détermine les méthodes invocables
et peut varier sur tout le chemin d'héritage jusqu'à
Object.
StandardAccount cs = new BonusAccount(new Person("A"), "a", 0.03);
// type réel : BonusAccount — type effectif : StandardAccountLe transtypage ne modifie pas l'objet, seulement la façon de le voir. L'upcast (vers
plus générique) est implicite et toujours valide ; le downcast doit être explicite et
peut échouer à l'exécution ; un cast entre classes sœurs est invalide —
(LimitedAccount) une référence vers un BonusAccount n'a aucun sens, même si elles
partagent un parent. instanceof est vrai pour toute la branche : BonusAccount,
StandardAccount et Object répondent tous true pour une instance de BonusAccount.
instanceof n'est pas une stratégie
instanceof étant vrai pour toute la branche héritée, brancher un traitement sur ce test
est le signe qu'une redéfinition (polymorphisme) serait plus propre.
Factoriser, étendre, spécialiser
Trois usages de l'héritage structurent le cours : factoriser l'état ou le
comportement (mutualiser les définitions), étendre (ajouter des méthodes pour
enrichir le comportement), spécialiser (redéfinir — overriding — pour altérer le
comportement). Dans une redéfinition, on peut augmenter la visibilité, jamais la
réduire. Le mot-clé super sert aux deux niveaux :
super(...)invoque le constructeur parent — première instruction obligatoire, exactement commethis(...);super.m()réutilise le traitement parent pour l'étendre :this.preprocessing(); super.first(); this.postprocessing();— c'est le motif deLimitedAccount.withdraw, qui contrôle le seuil avant de déléguer au retrait standard.
À ne pas confondre : l'overload (même nom, signatures différentes dans une même classe) se décide à la compilation ; l'override se décide à l'exécution.
Masquage d'attributs et lookup
Redéclarer un attribut dans la sous-classe ne le remplace pas : les deux champs
coexistent. Avec A b2 = new B() où B masque anAttribute (42 chez A, 404 chez B),
b1.anAttribute vaut 404 mais b2.anAttribute vaut 42 — l'accès direct dépend du type
effectif de la référence. Une méthode redéfinie, elle, exécute le code de B dans les
deux cas : la méthode invoquée dépend du type réel, l'attribut accédé du type
effectif.
Le lookup (mécanisme de recherche de méthodes) explique pourquoi. Quand la méthode
est public, la recherche part de la classe réelle de l'objet invoquant puis remonte
la hiérarchie jusqu'à trouver une définition — mécanisme dynamique et ascendant,
jamais vers les classes dérivées. Quand la méthode est private, elle est résolue
statiquement : c'est la définition visible là où le message est envoyé qui
s'exécute, sans égard au type réel. Enfin super.m() relance la recherche à partir de
la superclasse. Les traces d'exécution A→B→C du cours font pratiquer ces trois règles
sur des hiérarchies où les méthodes se redéfinissent partiellement.
La classe abstraite
Modéliser un jeu d'échecs fait surgir le cas limite : couleur, nom et position sont
communs à toutes les pièces, mais move() n'a pas de définition générale. Deux
mauvaises solutions : ne définir move que dans les sous-classes — impossible d'écrire un
traitement générique après un upcast vers ChessPiece —, ou déclarer des méthodes
creuses dans la mère : aucune sémantique, aucune garantie de redéfinition, et quel
sens aurait une instance de ChessPiece — une pièce qui n'est ni tour, ni fou, ni pion ?
La solution : abstract class. Une classe abstraite comporte attributs et méthodes comme
n'importe laquelle, peut déclarer des méthodes abstract sans corps (incompatibles
avec private), ne peut pas être instanciée — new est interdit, mais un constructeur
reste possible, appelé par super(...) — et n'a pas d'attribut abstrait.
public abstract class BankAccount {
protected double balance;
public void credit(double amount) { this.balance += amount; }
public abstract void withdraw(double amount); // dépend de la nature du compte
}Les classes dérivées doivent concrétiser toutes les méthodes abstraites, sous peine d'être abstraites elles-mêmes — dans l'exemple W-X-Y-Z du cours, Y reste abstraite par choix et Z, vide, est la seule classe de la hiérarchie instanciable. En UML, tout élément abstrait s'écrit en italique (ou précédé de «abstract» à la main).
A retenir
extends+super: ne spécifier que les changements, réutiliser l'initialisation et les traitements ;protected: l'entre-deux pensé pour l'héritage ;- type réel → code exécuté (lookup dynamique si
public, statique siprivate) ; type effectif → messages autorisés et attributs accédés ; - l'abstrait décrit ce qu'on sait faire, sans dire comment.
Parce qu'il crée deux champs indépendants dont l'accès dépend du type de la référence : le même objet semble porter deux valeurs différentes selon la variable utilisée. Encapsuler derrière des accesseurs — qui se redéfinissent proprement — évite toute ambiguïté.
Non. Elle n'est même pas visible des sous-classes : l'écrire à nouveau dans une classe dérivée crée une nouvelle méthode sans lien avec l'originale. Le compilateur n'objecte pas, mais le late binding ne joue pas.
Parce qu'elle sera quand même initialisée — comme superclasse. new BankAccount(...)
est interdit, mais super(p, number) dans StandardAccount exécute bien le
constructeur de la classe abstraite pour initialiser les attributs hérités.
Quand des méthodes sont présentes dans toutes les sous-classes avec des implémentations différentes et non définissables au niveau parent : seules les signatures se factorisent, et l'abstraction garantit à la fois la redéfinition et l'impossibilité d'instancier la classe générique.
coursOO_05.pdf
R2.01 · Cours 5 — Héritage et polymorphisme · 26 diapositives
Chargement du document…
coursOO_06.pdf
R2.01 · Cours 6 — Mécanismes de l'héritage · 26 diapositives
Chargement du document…
Dernière mise à jour le
Ch. 4 — Encapsulation, surcharge, polymorphisme
Encapsulation par délégation et ses quatre objectifs, surcharge et chaînage de constructeurs, ArrayList face au tableau — puis la mécanique du polymorphisme : type de la référence, classe réelle et late binding.
Ch. 7-8 — Interfaces et collections
Le contrat d'interface et l'implémentation multiple contre le problème du diamant, List/Set/Map et leurs complexités, le contrat equals/hashCode, les parcours avec Iterator, Comparable/Comparator et final sous toutes ses formes.