Héritage


L'héritage est un mécanisme qui a été introduit dans la programmation objet dans le but d'éviter la réécriture inutile de code lorsqu'une classe n'est qu'un cas spécial d'une autre classe:

Heritage.jpg, 35kB

La classe plus spécialisée est appelée sous-classe (ou classe fille)et la classe plus générale, classe mère.

Lorsqu'une relation d'héritage est définie entre deux classes, la sous-classe hérite de tous les attributs et méthodes de la classe mère. C'est à dire que tout se passe comme si les attributs et méthodes de la classe mère avaient été explicitement définies pour la sous-classe.

Une sous-classe d'une classe donnée représente donc une sorte de cas particulier de cette classe qui possède des attributs et méthodes supplémentaires et spécifiques.

Notez que la relation d'héritage entre classes peut se faire à plusieurs niveaux: la classe mère d'une classe, peut également avoir une classe mère ... etc.

Prenons le cas d'un héritage à trois niveaux: la classe C hérite de la classe B, qui elle même hérite de la classe A:

Heritage-Multi-Niveau.jpg, 18kB

Dans ce cas, la classe C héritera de tous les attributs et méthodes de la classe B, mais également de ceux de la classe A. A et B sont des classes ascendantes de la classe C et inversement B et C sont des classes descendantes de la classe A.

De manière générale, une classe hérite donc des attributs et méthodes de toutes ses classes ascendantes.

La redéfinition de méthode

La redéfinition de méthode (ou overriding en anglais) est une notion liée à l'héritage: c'est le fait de pouvoir redéfinir une méthode déjà définie dans la classe mère.

Redefinitionjpg.jpg, 57kB

Nous verrons à travers un exemple comment cela se traduit en Java.

Heritage en Java

En Java, pour déclarer qu'une classe B est une sous-classe d'une classe A, on utilise le mot clef extends dans la déclaration de la classe B:

 class B extends A {
     ....
 }
La classe Object

D'autre part, toute classe Java hérite de la classe Object. Cette classe prédéfinie représente un objet de manière générale. Elle possède entre autre une méthode nommée toString, qui converti un objet en chaine de caractères ainsi que deux méthodes pour comparer des objets: la méthode equals et la méthode compareTo. Comme nous l'avons déjà vu, ces deux dernières sont redéfinies dans la classe String.

Les méthodes de la classe Object n'ont pas beaucoup d'intérêt en elles mêmes. Par contre, nous verrons dans le deuxième cours de programmation objet qu'elles ont une utilité dans la définition de classes génériques.

Présentation de l'exemple

L'exemple de projet qui nous servira à introduire les notions d'héritage et de redéfinition de méthode se trouve dans le dossier Exemple-Java-ProgObjet1/Formes.

Il contient la définition d'une classe Cercle comme sous-classe d'une autre classe nommée Forme.

Toute forme possède une aire. Un cercle est une forme spéciale qui possède également un rayon.

Pour éviter les confusions, une instance de la classe Forme sera appelé par la suite forme quelconque. Par contre, une forme sans autre précision désignera soit une forme quelconque soit un cercle.

Voici, l'interface graphique de ce projet:

Formes.jpg, 35kB
Le bouton Nouvelle Forme

Le bouton Nouvelle Forme permet de générer une forme de deux manières:

Chaque forme générée, que ce soit une forme quelconque ou un cercle, est stockée dans un même tableau TF.

Notez qu'il est impossible en principe de stocker dans un même tableau des données de types différents. Nous verrons un peu plus loin ce qui permet ce miracle.

L'ensemble des formes contenues dans le tableau TF est affichée dans une zone de texte. Chaque ligne affichée dans cette zone décrit une des formes contenues dans le tableau, en précisant son numéro (l'indice de l'élément du tableau dans lequel elle est rangée), sa classe , son aire et s'il s'agit d'un cercle, son rayon.

Le bouton Afficher

Le bouton Afficher permet d'afficher les attributs d'une forme donnée par son numéro dans les champs de texte libellés Aire, Classe et Rayon.

La figure précédente montre l'affichage de la forme numéro 1: c'est une instance de la classe Forme dont l'aire est égale à 200.

Le bouton Afficher Tout

En l'abscence d'une aire minimale et d'un rayon minimal, ce bouton affiche toutes les formes en mémoire.

Si l'utilisateur précise une aire minimale, seules les formes ayant une aire supérieure ou égale à celle-ci sont affichées.

Dans les copies d'écran suivantes les formes contenues en mémoire sont les quatre formes de la figure précédente. Voici le résultat avec une aire minimale égale à 100:

AireMin-Formes.jpg, 36kB

Si l'utilisateur précise un rayon minimal, mais par d'aire minimale, toutes les formes quelconques sont affichées ainsi que les cercles dont le rayon dépasse le rayon minimal:

Ole-Johan-Dahl.jpg, 27kB

Enfin, s'il précise à la fois une aire minimale ainsi qu'un rayon minimal les deux critères de sélection sont pris en compte simultanément:

RayonEtAireMin.jpg, 32kB


La classe Forme

Commencons par la classe mère, c'est à dire la classe Forme dans notre exemple. Voici sa déclaration (pour l'instant nous ommettons les instructions contenues dans les méthodes afin de ne pas rentrer tout de suite dans les détails):

 public class Forme {
  double Aire;
  
  Forme (double a) { ... }
  
  static void AjouterForme (Forme f) { ... }  
    
  String Classe ()  { ...  } 
      
  String Description ()  { ...  }
    
  boolean AireSuperieure (double a){ ... }
}

Une forme quelconque ne possède donc que l'attribut Aire. Le constructeur initialise cet attribut avec l'aire a passé en paramètre.

La méthode statique AjouterForme ajoute une forme f dans le tableau TF. D'après la règle de compatibilité entre paramètres formels et paramètres effectifs que nous avons vus dans le cours dédiés aux sous-programmes, cette méthode ne permet pas à priori d'ajouter un cercle dans TF. Pourtant si ! Nous verrons comment un peu plus loin.

La méthode AireSuperieure retourne true si la forme à laquelle est elle appliquée a une aire supérieure à a.

Nous reviendrons ultérieurement sur le rôle des deux méthodes restantes : Classe et Description.

La classe Cercle

Voici la déclaration de la classe Cercle en tant que sous-classe de la classe Forme (en omettant comme avant le code des méthodes) :

 public class Cercle extends Forme {
    double Rayon;

    Cercle (double r,double a) { ... }
                                
    String Classe ()  { ... }
     
    static double CalculerAire (double r)  { ... }
       
    String Description ()  { ... }  
    
    boolean RayonSuperieur (double r){ ... }     
}

Dans la première ligne, le mot clé extends sert à préciser que cette classe est une classe fille de la classe Forme et que par conséquent elle hérite de tous les attributs et de toutes les méthodes de la classe Forme.

Attributs de la classe Cercle

Elle a un attribut supplémentaire Rayon de type double et hérite de l'attribut Aire de la classe Forme. Par conséquent, bien qu'il ne soit pas défini explicitement dans la classe Cercle, l'attribut Aire est bien un attribut de cette classe. Autrement dit la classe Forme possède deux attributs: Rayon et Aire.

Constructeur de la classe Cercle

Le constructeur de la classe Cercle possède deux paramètres: r et a de type double qui servent respectivement à initialiser le rayon et l'aire d'un nouveau cercle.

Méthodes de la classe Cercle

La classe Cercle possède six méthodes. Parmis elles, deux sont héritées de la classe Forme: il s'agit de la méthode statique AjouterForme et de la méthode AireSuperieure.

Les quatre autres méthodes sont spécifiques:

Classe effective

Avant d'entrer dans le fonctionnement des différentes méthodes des classes Cercle et Forme et la manière dont elles exploitent l'héritage, nous allons tenter de répondre aux questions qui sont restées jusqu'ici sans réponse:

Supposons que je déclare un objet f de classe Forme:

 Forme f;

Forme est dans ce cas la classe déclarée de f. Mais rappelons que f n'est pas réellement un objet de la classe Forme, mais plus exactement l'adresse d'un objet de cette classe, ou en terme de programmation objet, une référence à un objet de cette classe.

Etant donné que toutes les adresses occupent la même place mémoire, il est techniquement possible d'affecter à une référence d'objet, une référence d'objet d'une autre classe !

Cette possibilité est exploitée par la programmation orienté objet pour pouvoir stocker des références d'objets de différentes classes dans une même variable. Voici une manière de l'utiliser dans notre exemple:

 Forme f = new Cercle(r,a);

après cette instruction f contiendra donc une référence à un objet de la classe Cercle, bien qu'il soit déclaré en tant que référence d'objet de la classe Forme. Nous dirons dans ce cas que Cercle est la classe effective de f.

En Java, la classe effective d'un objet doit forcément être une classe descendante de la classe déclarée. L'instruction suivante sera donc rejetée par le compilateur:

  Cercle c = new Forme (a);

car Forme n'est pas une classe descendante de Cercle.

Notez que nous trouvons ici une nouvelle exception à la règle de validité d'une affectation

Revenons à présent plus précisement aux deux questions qui nous préoccupaient.

Le tableau TF

Le tableau TF de notre exemple nous sert à stocker toutes les formes générées par le programme. Il est gèré comme une pile avec un indice de fin (NF). Voici sa déclaration:

static final int M = 10;
static Forme TF[] = new Forme [M];
static int NF=-1;

D'après cette déclaration, chaque élément de TF est une référence à un objet de la classe Forme. Mais comme nous venons de le voir, Java autorise de lui affecter une référence à un objet d'une classe descendante de Forme, donc en particulier l'adresse d'un objet de la classe Cercle.

Cela explique comment TF peut contenir des formes quelconques et des cercles !

La méthode AjouterForme

Voici le code de cette méthode:

  static void AjouterForme (Forme f) {
   Formes.NF++;
   Formes.TF[Formes.NF] = f; } 

Le paramètre formel étant de type Forme, il est à priori impossible d'utiliser cette méthode pour ajouter un Cercle dans TF. Mais, comme nous venons de le voir, le paramètre effectif peut très bien être un cercle par sa classe effective et une forme quelconque par sa classe déclarée. Par exemple comme ceci:

  Forme c = new Cercle (r,a);
  Forme.AjouterForme (c);
 

dans ce cas les classes déclarées du paramètre formel et du paramètre effectif sont les mêmes et nous avons réussi à passer un cercle en paramètre sans contredire la règle de compatibilité entre paramètre formel et effectif !

En fait, Java autorise même que les classes déclarées des paramètres formels et effectif soient distinctes. Le compilateur acceptera par exemple, les instructions suivantes:

  Cercle c = new Cercle (r,a);
  Forme.AjouterForme (c);
 

car la classe du paramètre effectif (c de type Cercle) est une sous-classe de celle du paramètre formel (f de type Forme).

Les constructeurs

Pour éviter de réécrire inutilement des instructions, il peut être intéressant d'exécuter le constructeur de la classe mère dans celui de la classe fille. En Java ceci est rendu possible à l'aide du mot clef super.

Nous avons utilisé cette possibité dans notre projet. Voici le constructeur de la classe Forme:

 Forme (double a) {
   Aire = a; }  

et voici celui de la classe Cercle:

Cercle (double r,double a) {
  super (a);
  Rayon = r; } 

l'instruction super (a) appelle le constructeur de la classe mère (donc Forme dans notre cas) avec l'aire a. Dans notre exemple nous n'avons rien gagné en nombre de lignes de code puisque le constructeur de la classe Forme ne contient qu'une seule instruction (Aire = a), mais cela peut être utile dans des cas plus complexes. Si vous utilisez cette méthode, notez que l'appel du constructeur de la classe mère doit être obligatoirement la première instruction du constructeur de la classe fille.

Les méthodes redéfinies

Rappelons qu'une méthode est redéfinie si elle est déclarée à la fois dans la classe mère et dans la classe fille avec les même types de paramètres. Dans notre exemple, nous en avons deux: il s'agit des méthodes Classe et Description.

Prenons un objet f dont la classe déclarée est Forme et la classe effective Cercle:

 Forme f = new Cercle (r,a);

Si nous appliquons une méthode redéfinie à f, quelle va être la méthode appelée? celle de la classe fille (Cercle) ou celle de la classe mère (Forme) ?

La réponse est : celle de la classe fille. De manière générale pour une méthode surchargée, c'est la classe effective d'un objet qui détermine la méthode appelée.

A présent, voyons comment cela se traduit concrètement avec les méthodes Classe et Description de notre projet.

La méthode Classe

La méthode Classe nous sert à déterminer le nom de la classe à laquelle appartient une forme dont la classe effective est à priori inconnue. Pour cela nous avons utilisé la redéfinition de méthode.

Dans la classe Forme, la méthode Classe est définie comme suit:

String Classe () { return "Forme"; } 

et dans la classe Cercle, comme ceci:

String Classe () { return "Cercle"; } 
La méthode Description

La méthode Description retourne une chaine de caractères décrivant une forme de classe effective à priori inconnue. En redéfinissant cette méthode, nous lui donnons deux comportements possibles: pour une forme quelconque, elle donnera son aire et pour un cercle, son aire et son rayon. Ceci nous permettra en particulier d'afficher de manière élégante toutes les formes en mémoire (donc contenues dans le tableau TF) dans la zone de texte de notre application (ZT_Formes) par une simple boucle for ne contenant aucun test:

for (i=0; i<= NF;i++) {
    es.Afficher(TF[i].Description(), ZT_Formes); }

Voyons à présent comment sont codées les deux versions de la méthode Description.

Dans la classe Forme, elle est déclarée comme suit:

String Description () {
  return Classe() + " d'aire "+ Math.floor(Aire); }

Rappelons que Math.floor retourne la partie entière, c'est à dire le plus petit entier supérieur ou égale à x. Nous l'avons utilisée ici pour réduire la taille de l'affichage.

Dans la classe Cercle, Description est déclarée comme ceci:

String Description () {
   return super.Description()+" et de rayon "+ Rayon; } 

Nous avons ici une nouvelle utilisation du mot clé super: lorsqu'une méthode est redéfinie, il est possible d'appeler la méthode de la classe mère depuis la classe fille en faisant précéder son nom de super. L'intéret de ceci est d'écrire moins de lignes de code et aussi, de faciliter la mise à jour d'un projet, dans la mesure où toute modification de la méthode de la classe mère sera automatiquement récupercutée sur celle de la classe fille.

Voyons à présent comment ces méthodes s'exécutent selon la classe effective de l'objet auquel elles s'appliquent.

Considérons un élément TF[i] du tableau TF. La classe effective de cet élément peut être Forme ou Cercle.

Dans le premier cas, TF[i].Description () exécutera la méthode Description de la classe Forme. La méthode Classe() retournera dans ce cas la chaine "Forme". En supposant que l'aire de cette forme soit égale à 100, la chaine retournée sera "Forme d'aire 100".

Dans le deuxième cas, TF[i].Description () exécutera la méthode Description de la classe Cercle. La méthode Classe() retournera dans ce cas la chaine "Cercle". Supposons que ce soit un cercle de rayon 2 et d'aire 12 (partie entière). super.Description() retournera la chaine "Cercle d'aire 12". Le résultat retourné sera donc "Cercle d'aire 12 et de rayon 2".

Cast et instanceof

Pour le bouton Afficher (voir fenetre de l'application), nous avons besoin d'afficher les attributs d'une forme donnée f déclarée en tant que Forme dans les champs de texte associés: son aire, sa classe et uniquement s'il s'agit d'un cercle, son rayon.

Cela pose deux problèmes:

  1. Comment savoir si f est un cercle ou une forme quelconque ? Ou plus précisément, comment savoir si le type effectif de f est Cercle ou Forme?
  2. S'il s'agit d'un cercle, comment accéder à son rayon ? En effet, la notation f.rayon sera rejetée par le compilateur, car f est déclarée en tant qu'objet de la classe Forme !

Une manière de résoudre le premier problème est d'utiliser l'opérateur instanceof et pour le second de faire un cast.

instanceof permet de savoir si un objet appartient à une classe donnée, ou plus précisément si une classe donnée est bien la classe effective d'un objet.

De manière générale, le cast permet de convertir une expression E d'un certain type T1, en une expression d'un autre type T2. Cela s'écrit (T2) E.

En programmation objet, le cast est souvent utilisée pour convertir une référence à un objet d'une certaine classe A, en une référence à un objet d'une classe descendante de A. Par exemple, si f est déclarée en tant que référence à un objet de la classe Forme, l'écriture (Cercle) f représente f en tant que référence à un objet de la classe Cercle et par conséquent, si la classe effective de f est Cercle, on pourra accéder à son rayon par ((Cercle)f).Rayon. En fait, tout se passe comme si nous avions redéclaré localement f en tant que référence à un objet de la classe Cercle.

Revenons en à notre problème concernant le bouton Afficher et voyons comment il est résolu à l'aide du cast et de l'opérateur instanceof.

Voici la procédure évènementielle du bouton:

private void BT_AfficherActionPerformed(...) {                                                
  int i;
  i = es.LireEntier(CT_Num);
  AfficherAttributs(TF[i]); } 

Elle appelle donc la méthode AfficherAttribut en lui passant en paramètre l'élément TF[i] à afficher. Voici le code de cette méthode:

void AfficherAttributs (Forme f) {
  es.Afficher(f.Aire, CT_Aire);
  es.Afficher(f.Classe(), CT_Classe);
       
  if (f instanceof Cercle)     
      es.Afficher (((Cercle)f).Rayon,CT_Rayon); }

La valeur de l'expression (f instanceof Cercle) sera true si et seulement si la classe effective de f est Cercle. C'est donc uniquement dans ce cas que le rayon sera affiché et pour l'afficher nous faisons un cast, afin de convertir f en une référence à un cercle.

Nous venons de voir comment utiliser le cast pour accéder à un attribut de la classe effective d'un objet. On peut également l'utiliser pour appliquer à un objet une méthode spécifique (donc non héritée) et non redéfinie de sa classe effective.

Nous utilisons ceci dans la méthode AfficherLesFormes pour afficher la description de toutes les formes contenues de le tableau TF dont l'aire est supérieure à l'aire minimale et, pour les cercles uniquement, dont le rayon est supérieur au rayon minimal:

void Afficher_Les_Formes () { 
int i; boolean Afficher;
double Amin = 0, Rmin = 0;
      
if (!es.ChampDeTexteVide(CT_AireMin))
  Amin = es.LireDouble(CT_AireMin);
      
if (!es.ChampDeTexteVide(CT_RayonMin))
  Rmin = es.LireDouble(CT_RayonMin);
           
es.Afficher(NF+1, CT_NF); 
  
es.Effacer(ZT_Formes);
        
for (i=0; i<= NF;i++) {
  Afficher = TF[i].AireSuperieure(Amin);
  if (TF[i] instanceof Cercle)
      Afficher = Afficher && ((Cercle)TF[i]).RayonSuperieur(Rmin);
            
  if (Afficher)  es.Afficher(i+":"+TF[i].Description(),ZT_Formes);
  }
}

Etant donné que TF est un tableau de références à des objets de la classe Forme, le problème ici se situe dans l'application de la méthode RayonSuperieur (méthode non redéfinie et spécifique de la classe Cercle) à un élément TF[i] du tableau TF. Pour cela, nous testons tout d'abord si la classe effective de TF[i] est Cercle grâce à l'opérateur instanceof. Puis, s'il s'agit bien d'un cercle, nous transformons TF[i] en une référence à un cercle, via le cast (Cercle)TF[i]), ce qui nous permet de lui appliquer la méthode RayonSuperieur afin de tester si son rayon est bien supérieur au rayon minimal.