Héritage et polymorphisme
Comprendre l’héritage
Le mécanisme de l’héritage est largement utilisé en programmation orientée objet et il est important d’en rappeler l’utilité.
Hériter d’une classe revient à spécialiser certains de ses comportements et certaines de ses propriétés tout en profitant de ses services de base et, ainsi, éviter toute redondance de code.
Java et C# ne permettent qu’un seul héritage par niveau (contrairement à C++), mais il est possible d’hériter d’une classe elle-même déjà héritière, et ainsi de suite, pour constituer une hiérarchie de classes partant de la plus globale à la plus détaillée.
Exemple de hiérarchie de classes
Par défaut, une classe peut servir de parent à plusieurs classes. Le meilleur exemple est, sans aucun doute, java.lang.Object qui est la classe racine de tous les types - et donc de toutes les classes - de Java. Rappelons que cet héritage est implicite et ne nécessite aucune déclaration particulière.
Codage de la superclasse (classe de base) et de sa sous-classe (classe héritière)
Le code d’une classe définit les règles de son éventuel héritage.
1. Interdire l’héritage
Tout d’abord, est-il souhaitable de rendre une classe « héritable » ? Si l’analyse démontre que non, alors le mot-clé final doit être utilisé dans la définition de la classe pour en interdire tout héritage.
Syntaxe de déclaration d’une classe finale
[visibilité] final class NomClasse
{
//...
}
Exemple de classe « finale »
public final class Directeur {
//...
}
Comme le montre la capture suivante, IntelliJ IDEA refuse de compiler une classe étendant la classe finale Directeur :
La classe Java String est une classe de type « final ».
Une classe ne contenant pas le mot-clé final dans sa définition est considérée comme extensible.
2. Définir les membres héritables
Une superclasse choisit ses membres « transmissibles » grâce à leurs attributs d’accessibilité. Ainsi, les classes héritières (sous-classes)...
Communication entre classe de base et classe héritière
1. Les constructeurs
Lorsqu’une classe héritière est instanciée, le constructeur de sa superclasse est appelé avant le sien. Voici un extrait de code suivi du résultat console qui en témoigne.
package com.eni;
// Le point d'entrée de notre exemple
public class Main {
public static void main(String[] args) {
DemoHeritage dh = new DemoHeritage();
dh.Test();
}
}
package com.eni;
// La classe Demoheritage va...
public class DemoHeritage {
// ... au travers de sa méthode Test
public void Test(){
//... montrer l'instanciation de l'héritière
System.out.println("Instanciation d'une ClasseEnfant");
ClasseEnfant classeEnfant = new ClasseEnfant();
//
}
}
package com.eni;
// Définition d'une classe héritière
// de la superclasse ClasseParent
public class ClasseEnfant extends ClasseParent {
// avec des propriétés public, protected et private
public String PublicPropClasseEnfant;
protected String ProtectedPropClasseEnfant;
private String PrivatePropClasseEnfant;
public ClasseEnfant() {
System.out.println("Ctor Classe Enfant");
}
}
package com.eni;
// Définition de la "superclasse"...
public class ClasseParent {
// avec des propriétés public, protected et private
public String PublicPropClasseParent;
protected String ProtectedPropClasseParent; ...
Exercice
1. Énoncé
Créez une nouvelle solution de type console.
Ajoutez une classe CompteBancaire représentant un… compte bancaire et ayant les propriétés suivantes :
-
Titulaire (String) ;
-
Numéro (Integer contenant une valeur unique attribuée à l’instanciation ; le premier numéro de compte sera 100) ;
-
Solde (double).
Et les méthodes suivantes :
-
Créditer (permet de déposer de l’argent sur le compte) ;
-
Débiter (permet de retirer de l’argent du compte) ;
-
Relever (affiche toutes les informations sur le compte).
Ajoutez une classe CompteBancaireRemunere représentant un compte bancaire rémunéré héritant de la classe CompteBancaire et dont le constructeur prendra comme paramètres le nom du titulaire et le taux de rendement du compte.
Redéfinissez la méthode Créditer de CompteBancaireRemunere pour que le montant déposé soit augmenté du taux de rémunération défini dans le constructeur. Ne rêvons pas, ce fonctionnement bancaire atypique est uniquement fait pour simplifier l’exercice.
Codez dans le main une séquence permettant de vérifier le fonctionnement des classes.
2. Corrigé
package labcomptebancaire;
public class CompteBancaire {
// Entier de type static mémorisant
// le compteur global de numéro de compte
private static Integer numCount = 100;
// Nom du titulaire
private String Titulaire;
public String getTitulaire() {
return Titulaire;
}
public final void setTitulaire(String Titulaire) {
this.Titulaire = Titulaire;
} ...
Les classes abstraites
Il peut arriver qu’une superclasse contienne des méthodes ne pouvant être implémentées car elles n’ont aucun sens sans un minimum de spécialisation…
Par exemple, une classe de base FormeGeometrique propose une méthode virtuelle Dessiner. Cette classe est ensuite étendue par les classes Triangle, Rectangle et Cercle qui vont remplacer et implémenter chacune leur propre méthode Dessiner.
L’implémentation de la méthode Dessiner dans la classe de base FormeGeometrique n’a donc aucun sens car chaque forme est spécifique et, au niveau de FormeGeometrique, cette forme est tout à fait abstraite !
Alors, dans ce cas, pourquoi définir la méthode Dessiner dans FormeGeometrique ?
Ceci est tout à fait en rapport avec le polymorphisme… Imaginez que vous soyez en train de construire une application de dessin et que cette application gère une série de formes géométriques soigneusement définies et enregistrées par l’utilisateur. Vous pourriez gérer une liste d’objets de type Triangle, une liste d’objets de type Rectangle, etc. Bon courage car cela deviendra très vite lourd… En construisant une liste d’objets de type FormeGeometrique, vous pourrez la remplir d’objets de types Triangle, Rectangle...
Le polymorphisme
1. Comprendre le polymorphisme
En programmation orientée objet, le polymorphisme permet à une classe héritière d’être présentée dans un traitement comme sa classe de base ou comme une de ses interfaces. Grâce à la virtualisation des méthodes, le traitement appelant la méthode de base est « transféré » dans la classe héritière. On obtient ainsi une spécialisation du traitement.
Rappel : toute classe dérivée peut implicitement être convertie en un objet de sa classe de base.
En langage Java, la virtualisation par défaut automatique a été détaillée dans les pages précédentes. Elle permet à la classe de base de définir des méthodes qui pourront être reprises par son ou ses héritières afin d’être spécialisées.
En langage Java, chaque objet est polymorphe. Il peut en effet être considéré comme son propre type ou comme celui de sa classe de base et, par bonds successifs, comme le type racine de tous les types, à savoir java.lang.Object.
2. Exploitation du polymorphisme
La programmation la meilleure est celle qui privilégie le moins de dépendance entre les modules. Une architecture composée de modules faiblement couplés (loosely coupled...