Agrégation et héritage - Résolution et liaison
Table of Contents
La grande question : Comment construire des interfaces ou des classes ?
- Par agrégation
- La relation "a un"
- Par héritage
- La relation "est un"
- Avantages de l'héritage : factorisation du code et polymorphisme
Le cas des interfaces
Agrégation : la brique de base
// Un entier naturel interface Nat { // - a pour valeur un int. int val(); // - peut être nul ou non boolean estNul(); // - a un prédécesseur (s'il est non nul) Nat predecesseur(); } // Une fabrique générique d'entiers naturels interface FabriqueNaturels<T> { // a une méthode fabriquant zéro. T creer(); }
Héritage : la puissance de la factorisation
- Possibilité d'un héritage multiple
- Préservation des méthodes des interfaces parentes
- Possibilité d'une extension par agrégation
// Un entier naturel est // - un élément d'un semi-anneau unitaire et // - une fabrique d'entiers naturels. public interface Nat extends SemiAnneauUnitaire<Nat>, FabriqueNaturels<Nat> { ... // Extension par agrégation } // Un semi-anneau unitaire est un semi-anneau, un monoide multiplicatif // et est biunifère. interface SemiAnneauUnitaire<T> extends SemiAnneau<T>, MonoideMultiplicatif<T>, BiUnifere<T> // Héritage multiple {}
Deux particularités de l'héritage
- Possibilité de spécialiser une méthode
// Cas typique d'une interface possédant des fabriques (méthodes de fabrication) interface Nat1 { Nat1 creer(); // Une fabrique ... } interface Nat2 extends Nat1 { Nat2 creer(); // Une fabrique spécialisant // (et remplaçant) Nat1 creer() ... }
- Covariance possible pour le type de retour
- Invariance pour le type des arguments (la contravariance aurait été possible mais les concepteurs du langage ont préféré permettre la surcharge)
- Possibilité d'une surcharge : méthodes ayant le même nom mais des types différents de paramètres
// Fabrique générique avec des méthodes de même nom interface FabriqueNat<T> { T creer(); T creer(T pred); T creer(int val); }
Le cas des classes
Agrégation : la brique de base (bis)
class NatParInt implements Nat { // Attributs private int val; // Constructeurs public NatParInt(int val){ if(val < 0) throw new IllegalArgumentException("Pas de Nat à patir d'un int négatif."); this.val = val; } // Méthodes (accesseurs, fabriques et services) @Override public Nat zero() { return this.creerNatAvecValeur(0); } ... }
Héritage : la puissance de la factorisation (bis)
- Héritage simple uniquement
- Préservation des attributs et des méthodes des classes parentes
- Possibilité d'une extension par agrégation
// Implémentation des entiers naturels non nuls (des successeurs) class Succ implements Nat { private Nat predecesseur; public Succ(Nat predecesseur) { this.predecesseur = predecesseur; } @Override public int val() { return 1 + this.predecesseur().val(); } } // Variante mémorisant la valeur de l'entier naturel class SuccMemoire extends Succ implements Nat { // Extension de l'état private int val; // Constructeur public SuccMemoire(Nat predecesseur) { super(predecesseur); // Appel du constructeur parent this.val = this.init(); } // Extension des services private int init() { ... } }
La possibilité d'un héritage multiple
A partir de Java 8, il est possible d'implémenter les méthodes déclarées dans une interface. Elles doivent alors être précédées du qualificatif default. On parle de trait en certains langages, notammeent Scala, mais nous utiliserons plutôt dans ce cours le terme d'interface concrète, par analogie avec celui de classe abstraite.
interface Nat { default Nat somme(Nat x){ return ...; } }
Il devient possible d'obtenir l'héritage multiple par une construction simple, en suivant la méthode de définition d'une classe d'implémentation. Toute classe X se décompose en trois types :
- une classe E réifiant l'état (c'est-à-dire représentant l'état par un objet), qui agrège les attributs, les constructeurs et les accesseurs,
- une interface I déclarant les accesseurs et les fabriques, non implémentés, et les services, implémentés,
- une classe de composition C, équivalente à celle initiale X,
- agrégeant un objet de la classe E définissant l'état,
- implémentant les contructeurs en initialisant l'état,
- implémentant les accesseurs par délégation à l'état,
- implémentant les fabriques en appelant les contructeurs,
- implémentant l'interface I et ainsi héritant du code des services.
Cette construction se généralise facilement à l'héritage multiple. Supposons des classes Xj décomposées en (Ej, Ij, Cj). La classe M correspondant à l'héritage multiple des Xj se construit comme la classe de composition précédente :
- agrégation d'objets des classes Ej pour définir l'état,
- implémentation des constructeurs en initialisant l'état,
- implémentation des accesseurs par délégation à l'état,
- implémentation des fabriques en appelant les contructeurs,
- implémentation des interfaces Ij et ainsi héritage du code des services.
Lorsque deux méthodes identiques (même nom, même type pour les arguments) sont héritées de deux interfaces, le conflit doit être levé explicitement, en redéfinissant la méthode. Pour accéder à une méthode f définie dans une interface parente P, on doit utiliser la notation P.super.f.
Enfin, les méthodes de la classe Object ne peuvent être définies dans une interface. Les concepteurs du langages Java ont fait le choix de considérer ces méthodes comme des méthodes de bas niveau, devant accéder aux attributs : elles ne peuvent être définies que dans les classes. Si on considère qu'une de ces méthodes est plutôt de haut niveau, comme equals, et doit être implémentée dans une interface, il est nécessaire de la doubler par une seconde méthode, estEgal(Object o) par exemple.
Une particularité très importante : la spécialisation
- Redéfinition d'une méthode
class Succ implements Nat { ... @Override public int val() { return 1 + this.predecesseur().val(); } } class SuccMemoire extends Succ implements Nat { ... @Override public int val() { return this.val; } }
Effet : remplacement de la méthode spécialisée par la méthode spécialisante
Pas d'effet lors d'une conversion
SuccMemoire b = new SuccMemoire(...); Succ a = b; a.val() // -> accès à val dans SuccMemoire
- Possibilité d'appeler la méthode spécialisée de la classe parente avec super
class SuccMemoire extends Succ implements Nat { ... // Extension des services private int init() { return super.val(); // Appel de la méthode spécialisée dans Succ } //
Deux particularités secondaires
Masquage des attributs
class A { public int x; } class B extends A { public int x; }
Les deux attributs existent dans un objet de la classe B mais celui déclaré dans B masque celui déclaré dans A.
Comment accéder au x déclaré dans A ?
Par conversion.
B b = new B(); A a = b; a.x // -> accès au *x* déclaré dans *A*
Possibilité d'une surcharge
Cf. les interfaces.
Le polymorphisme
La relation de sous-typage
- Relation de sous-typage = fermeture réflexive et transitive de la
réunion \(\bigcup\)
- relation d'implémentation (implements)
- relation d'héritage (extends)
Règle de subsomption
\[\small \begin{array}{c} A \mathtt{\ sous-type\ de\ } B \quad \quad e \mathtt{\ de\ type\ } A \\ \hline e \mathtt{\ de\ type\ } B \\ \end{array} \]
FabriqueNaturels<Nat> fab = new Succ(...); // Classe Succ sous-type de interface Nat sous-type de interface FabriqueNaturels<Nat> Succ s = new SuccMemoire(...); // Classe SuccMemoire sous-type de classe Succ
La liaison tardive
- Un appel de méthode exp.f(args)
- A la compilation : résolution du nom f
- Hypothèse 1: l'expression cible exp a pour type C.
- Hypothèse 2: les arguments args ont pour type A (une liste de types).
- Conclusion : le nom f est résolu en une déclaration d'une méthode f(B b) (avec B sur-type de A) dans la table des méthodes associée au type C.
- A l'exécution : liaison du nom f
- Hypothèse 1 : l'expression cible exp s'évalue en c, une référence à un objet d'une classe D sous-type de C.
- Hypothèse 2 : les arguments args s'évaluent en a (une liste de valeurs).
Conclusion : le nom f est lié dans la table des méthodes de D à la méthode correspondant à la déclaration déterminée par la résolution. La méthode est appelée avec les arguments a.
\(\Rightarrow\) Possible appel d'une méthode spécialisante
Exercice - Résolution et liaison en pratique
Résolution 1
class Succ ... { ... public int val() { ... } // D1 } class SuccMemoire extends Succ ... { ... public int val() { ... } // D2 } ... public static void main(String[] args){ SuccMemoire x = ...; ... x.val() ...; // }
Liaison 1
class Succ ... { ... public int val() { ... } // D1 } class SuccMemoire extends Succ ... { ... public int val() { ... } // D2 } ... public static void main(String[] args){ SuccMemoire x = new SuccMemoire(); ... x.val() ...; // }
Résolution 2
class Succ ... { ... public int val() { ... } // D1 } class SuccMemoire extends Succ ... { ... public int val() { ... } // D2 } ... public static void main(String[] args){ Succ x = new SuccMemoire(); ... x.val() ...; // }
Liaison 2
class Succ ... { ... public int val() { ... } // D1 } class SuccMemoire extends Succ ... { ... public int val() { ... } // D2 } ... public static void main(String[] args){ Succ x = new SuccMemoire(); ... x.val() ...; // }