Héritage en JAVA (suite) - le Polymorphisme

Philippe Genoud (Philippe.Genoud@imag.fr)

dernière modification : mercredi 15 décembre 1998 

Objet :
Présentation des aspects "avancés" de l'héritage: polymorphisme, classes abstraites et interfaces .
bibliographie:
- chapitre 3 du livre "JAVA in a Nutshell" de David Flanagan, Ed. O'Reilly 1997.
- chapitres 6 et 7 du livre Thinking in JAVA, Bruce Eckel, à paraître Ed. Printice Hall, mai 1998

Sommaire :

Héritage (rappels)
SurClassement - Upcasting
Résolution des messages - liaison dynamique (dynamic binding)
A quoi servent l'upcasting et les liens dynamiques ?
Classes abstraites
Interfaces
 



Héritage - rappels
 
 
  • l'héritage : définir une nouvelle classe à partir d'une classe déjà existante
  • une classe A qui hérite d'une classe B est dite sous-classe de B, et inversement B est appelée la super-classe de A (ou parfois sa classe de base)
  • une sous classe peut  étendre et/ou spécialiser  les fonctionalités de sa super-classe
    • elle hérite des variables et méthodes définies dans sa super-classe
    • elle peut définir de nouvelles variables et méthodes
    • elle peut redéfinir (override) des méthodes dont elle hérite (c'est aussi possible pour les variables d'instance, mais cela est moins recommandé :-( ) 
    • la redéfinition d'une méthode peut se faire de deux manières :
      • en remplaçant complètement la méthode héritée par une nouvelle implémentation
      • en spécialisant la méthode héritée en proposant une nouvelle implémentation qui réutilise celle héritée (via super.nomDeLamethodeHéritée(paramètres))
 
exemple : reprenons notre exemple des étudiants vus au cours précédent
 
 
public class Etudiant { définition d'une classe de base
  String nom; 
  String prénom; 
  int age; 
  ...
attributs 
 
  public Etudiant(String n, String p, int a, 
      ...) 
  { 
     nom =s; 
     prénom = p; 
     age = a; 
     ... 
  } 
  ... 
 
constructeurs 
 
 
 
  public void affiche() 
  { 
    System.out.println("Nom : " + nom + + "\n" 
           "Prénom : " + prénom + "\n" 
           "Age : " + age + ...); 
  } 
  ...
méthodes 
 
 
 
 
}
class EtudiantSportif extends Etudiant { définition d'une sous-classe d'Etudiants
  String sportPratiqué; 
  ...
définition de nouveaux attributs
  public EtudiantSportif(String n, String p, 
    int a,..., String sport,..) 
  { 
    super(n,p,a,...); 
    sportPratiqué = sport; 
    ... 
  } 
  ...
constructeurs
  public double bonuSportif() 
  { 
     double bonus = ...; 
     return bonus; 
  } 
 
définition de nouvelles méthodes
  public void affiche() 
  { 
     super.affiche(); 
     System.out.println("Sport pratiqué : "+ 
          sportPratiqué + "\n" 
          + ...)
  }
redéfinition (overriding) de la méthode affiche 
héritée de Etudiant
}  
 
"Surclassement" -  Upcasting
 
  • La réutilisation du code est un aspect important de l'héritage, mais ce n'est peut être pas le plus important
  • Le deuxième point fondamental est la relation qui relie une classe à sa super-classe. Une classe A qui hérite de la classe  B peut être vue comme un sous-type (sous ensemble) du type défini par la classe B. En d'autres termes tout objet instance de la classe A peut être aussi vu comme une instance de la classe B.
  • Cette relation est directement supportée par le langage JAVA :
    • à une référence déclarée de type B il est possible d'affecter une valeur qui est une référence vers un objet de type A
    • plus généralement à une référence d'un type donné, il est possible d'affecter une valeur qui correspond à une référence vers un objet dont le type effectif est n'importe quelle sous-classe directe ou indirecte de B.
 
exemple :
 
Etudiant e = new EtudiantSportif("DUPONT","JEAN",25,..,"Badminton",..);
 
  • Lorsqu'un objet est "sur-classé" il est vu comme un objet du type de la référence utilisée pour le désigner
  • Ses fonctionnalités sont alors restreintes à celles proposées par la classe du type de la référence 
 
e.affiche(); autorisée, cette méthode fait partie de l'interface de la classe Etudiant
System.out.println("nom " + e.nom); autorisée, car nom est un attribut défini dans la classe Etudiant
System.out.println("sport pratiqué " + 
    e.sportPartiqué);
interdit car sportPratiqué est une variable définie dans la classe EtudiantSportif
double b = e.bonusSportif(); interdit car bonusSportif() est une méthode définie dans la classe EtudiantSportif
 
 
  • la possibilité d'affecter à une référence d'un type (d'une classe) donné une valeur qui désigne un objet du type de la référence ou de toute sous classe de ce type est désignée sous le terme de polymorphisme. (A l'exécution la référence peut désigner un objet qui prend des "formes" différentes selon sa classe effective)
Résolution des messages - liaison dynamique (dynamic binding)

A ce stade on peut se poser la question de l'intérêt d'une telle utilisation des types. Cet intérêt qui est fondamental en programmation orientée objet, vient de la manière dont les messages sont résolus (messages binding). Si on reprend l'exemple précédent quel code va être effectivement exécuté lorsque le message affiche() est envoyé à e ? Le code de la méthode définie dans la classe Etudiant ou celui de la méthode définie dans la classe EtudiantSportif qui est la classe effective de l'objet référencé par e ?

 
 
 
Etudiant e = new EtudiantSportif("DUPONT","Jacques", 
     25,..,"Karaté",..); 
e.affiche();
-->  
Etudiant Sportif 
nom: DUPONT 
prénom : Jacques 
age : 25 
... 
sport pratiqué : Karaté
 
  • Lorsqu'une méthode d'un objet est accédée au tavers d'une référence "surclassée", c'est la méthode telle qu'elle est définie au niveau de la classe effective de l'objet qui est en fait invoquée et exécutée
 
 
 
Comment est-ce possible ? 
  • Le compilateur ne dispose par de l'information nécessaire pour associer le code d'une méthode à un message : il ne connait pas le type exact de l'objet récepteur d'un message
  • En fait les messages sont résolus à l'exécution (run-time)
    • A cet instant le type exact de l'objet est connu
    • Il y un mécanisme pour retrouver le type d'un objet et appeler la méthode appropriée au moement de l'exécution
    • Ce mécanisme est désigné sous le terme de lien-dynamique (dynamic binding, late-binding ou run-time binding)
 
exmple :
soient trois classes : ClasseA, ClassesB qui hérite de ClasseA et ClasseC qui hérite de ClasseB. Génération de 10 objets dont l'appartenance à ClasseA, ClasseB ou ClasseC est déterminée de manière aléatoire et affichage des propriétés des objets instanciés.
 
ClasseA a; 

for (int i = 0; i < 10; i++) 
{ 
   hasard = Math.random() 
 
   if ( hasard < 0.33)

     a = new ClasseA(); 
   else if ( hasard < 0.66)
     a = new ClasseB();
  else 
     a =new ClasseC();
  System.out.println("Classe de a : "+ 
     a.getClass().getName()); 
 
découvre dynamiqument le nom de la classe
  a.affiche(); affiche de ClasseA, de ClasseB ou de ClasseC ?
}
 
A quoi servent l'upcasting et les liens dynamiques ?
 
  • l'upcasting et la liasion dynamique offrent toute sa dimension au polymorphisme qui comme le souligne Bruce Eckel dans son ouvrage "Thinking in JAVA" consitue "la troisième caractéristique essentielle d'un langage orienté objet après l'abstraction des données (encapsulation) et l'héritage"
  • "Once you know that all method binding in Java happens polymorphically via late binding, you can always write your code to talk to the base class, and know that all the derived-class cases will work correctly using the same code. Or put it another way, you send a message to an object and let the object figure out the right thing to do" Bruce Eckel, Thinking in Java
  • le polymorphisme associé à la liasion dynamique offre :
    • une plus grande simplicité du code (plus besoin de distinguer différents cas en fonction de la classe des objets),
    • et surtout une plus grande facilité d'évolution du code (les programmes sont plus facilement extensibles car il est possible de définir de nouvelles fonctionnalités en héritant de nouveaux types de données à partir d'une classe de base commune sans avoir besoin de modifier le code qui manipule l'interface de la classe de base)
exemple :
imaginons une classe qui manipule des ensembles d'étudiants par exemple pour représenter un groupe de TD
 
class Etudiant { définition d'une classe de base
  String nom; 
  String prénom; 
  int age; 
  ...
attributs 
 
  public void affiche() 
  { 
    System.out.println("Nom: "+ nom + 
        " Prénom : " + prénom); 
    System.out.println("Age: " +age); 
    ... 
  } 
  ...
méthodes 
 
 
 
 
}
class EtudiantSportif extends Etudiant { définition d'une sous-classe d'Etudiants
  String sportPratiqué; 
  ...
définition de nouveaux attributs
  public void affiche() 
  { 
    super.affiche(); 
    System.out.println("Sport pratiqué:" 
     +sportPratiqué); 
    ... 
  }
redéfinition (overriding) de la méthode affiche 
héritée de Etudiant
}  
class GroupeTD {
  Etudiant[] liste = new Etudiant[]; 
  int nbEtudiants = 0;
liste est un tableau d'Etudiants
  ...
  public void ajouter(Etudiant e)  
  { 
     if (nbEtudiants < liste.lenght) 
        liste[nbEtudiants++] = e; 
  }
Il est possible de ranger dans liste des Etudiants et aussi des EtudiantsSportifs (de manière générale  des objets de n'importe quelle sous classe directe ou indirecte d'Etudiant)
  public void afficherListe() 
  { 
    for (int i=0;i<nbEtudiants; i++) 
          liste[i].affiche(); 
  } 
}
Si de nouvelles classes d'Etudiants sont définies, le code de afficherListe()  (de même que celui de ajouter(Etudiant e)) n'a pas besoin d'être modifié pour prendre en compte ces modifications.  
D'ailleurs, si c'est le cas pour toutes les méthodes de la classe, celle-ci n'a même pas besoin d 'être recompilée.
 
Classes abstraites
 
  • Pour exploiter pleinement le polymorphisme il est courant de définir des classes dont le seul rôle est de spécifier une interface (un ensemble de méthodes) commune pour toutes les classes qui en seront dérivées.
  • Dans de telles classes les méthodes qui servent à définir cette interface commune sont le plus souvent muettes  (elles ne contiennent pas de code effectif)
  • Au niveau de l'exécution cela n'aurait pas de sens que de définir des instances de ces classes
  • Ces classes sont désignées parfois sous le terme de classes abstraites
 
exemple : le grand classique de la programmation objet et du polymorphisme : les formes géométriques
 
  • Le langage JAVA offre un support pour les classes abstraites.
  • Il est possible de définir une méthode sans en préciser l'implémentation en la spécifiant comme étant abstraite (modifieur abstract dans la signature (l'en tête) de la méthode).
  • Une méthode abstraite n'a pas de corps, elle est constituée simplement d'une signature suivie d'un ;
  • Toute classe qui contient une méthode abstraite est automatiquement considérée comme abstraite elle aussi et doit explicitement être déclarée comme telle.
  • Une classe abstraite ne peut être instanciée.
  • Une sous-classe d'une classe abstraite si elle redéfinit (overrides) et fournit une implémentation pour chacune des méthodes abstraites dont elle hérite est alors une classe "concrète" et peut être instanciée 
  • Si une sous-classe d'une classe abstraite n'implémente pas toutes les méthodes abstraites dont elle hérite, elle est elle-même une classe abstraite
 
exemple : syntaxe des classes et méthodes abstraites en JAVA
 
pubic abstract class Forme { définition de la classe de base comme étant abstraite
  double x,y; // position de la forme attributs 
 
  public void deplace(double x, double y) 
  { 
    this.x += x; this.y +=y; 
  } 
  ...
méthode "concrète" 
 
 
 
 
  public abstract double surface(); méthodes "abstraites"
  public abstract double périmètre();
}
class Cercle extends Forme { définition d'une sous-classe de Forme
  double r; définition des attributs propres aux cercles
  public double surface()  
  {  
      return (Math.PI * r * r); 
  }
redéfinition (overriding) des méthodes abstraites  
héritées de Forme
 public double périmètre()  
  {  
      return (2.0 * Math.PI * r); 
  }
  ... 
}
 
class Rectangle extends Forme { définition d'une sous-classe de Forme
  double largeur, hauteur; définition des attributs propres aux rectangles
  ...
  public double surface()  
  {  
      return (largeur * hauteur); 
  }
redéfinition (overriding) des méthodes abstraites  
héritées de Forme
 public double périmètre()  
  {  
      return (2.0 * (largeur + hauteur); 
  }
}
 
 
  • Bien entendu l'utilisation du nom d'une classe abstraite comme type pour une (des) référence(s) est toujours possible (et souvent souhaitable !!!)
 
exemple :
 
Forme[] lesFormes = new Forme[3]; déclaration d'un tableau pour stocker des objets Forme
lesFormes[0] = new Cercle(2.0); 
lesFromes[1] = new Rectangle(14.0,18.0); 
lesFromes[2] = new Rectangle(34.0,47.0);
intialisation du tableau avec différentes formes
double surfaceTotale = 0.0; 
for (int i=0; i < lesFormes.length; i++) 
  surfaceTotale += lesFormes[i].surface();
calcule de la surface totale des formes stockées dans le tableau
 
Interfaces

Parfois,  on aurait bien envi de faire de l'héritage multiple comme dans l'exempe ci-dessous :
 

 
  • La solution à ce problème en JAVA : les interfaces (protocoles dans d'autres langages)
  • Une interface est similaire à une classe abstraite, sauf que :
    • le mot clé interface remplace les mots clés abstract class
    • toutes les méthodes définies dans une interface sont implicitement abstraites
    • toutes les variables définies dans une interface doivent l'être en tant que constantes (static final)
 
 exemple :
 
public interface Dessinable { déclaration d'une interface
  public void afficher(Graphics g); définition des méthodes abstraites
  public void effacer(Graphics g);
}
 
 
  • De la même manière qu'une classe étend sa super-classe elle peut de manière optionnelle implémenter une ou plusieurs interfaces
  • Pour implémenter une interface dans une classe JAVA il faut :
    • dans la définition de la classe, après la clause extends nomSuperClasse, faire aparaître explicitement le mot clé implements suivit du nom de l'interface implémentée 
    • fournir une implémentation (un corps) à chacune des méthodes abstraites définies dans l'interface
 
exemple :
 
public class RectangleDessinable extends Rectangle implements Dessinable { déclaration d'une interface
  public void afficher(Graphics g) 
  { 
     g.drawRect(x,y,largeur,hauteur); 
  }
implémentation des méthodes abstraites 
de l'interface Dessinable
  public void effacer(Graphics g) 
  { 
     g.clearRect(x,y,largeur,hauteur); 
  }
}
 
 
  • Comme les classes, en JAVA les interfaces peuvent être utilisées comme type de données.
  • A des variables (références) dont le type est une interface il est possible d'affecter des instances de toute classe implémentant l'interface, ou toute sous-classe d'une telle classe.
 
exemple :
 
Dessinable[] lesFormesDessinables = new Dessinable[3]; déclaration d'un tableau pour stocker des objets Dessinable
lesFormesDessinables[0] = new CercleDessinables(2.0); 
lesFromesDessinables[1] = new RectangleDessinables(14.0,18.0); 
lesFromesDessinables[2] = new RectangleDessinables(34.0,47.0);
intialisation du tableau avec différentes formes
double surfaceTotale = 0.0; 
for (int i=0; i < lesFormesDessinables.length; i++) 
{ 
  surfaceTotale += ((Forme) lesFormes[i]).surface();  
  lesFormesDessinables[i].afficher(g); 
}
affiche les formes stockées dans le tableau 
et calcule leur surface totale
 
  • Une classe JAVA peut implémenter simultanément plusieurs interfaces
  • Pour cela la liste des noms des interfaces à implémenter séparés par des virgules doit suivre le  mot clé implements
 
exemple
 
public class FormeDessinablePersistant extends Dessinable, Persistant 
{
    // implementation des methodes de l'interface Dessinable 
     ..... 

    // implementation des méthodes de l'interface Persistant 
     ..... 

}

Réutilisation des interfaces
 
  • De la même manière qu'une classe peut avoir des sous-classes, une interface peut avoir des "sous-interfaces"
  • Une sous interface
    • hérite de toutes les méthodes abstraites et des constantes de sa "super-interface"
    • peut définir de nouvelles constantes et méthodes abstraites
  • A la différence des classes une interface peut étendre plus d'une interface à la fois
  • Une classe qui implemente une interface doit implémenter toutes les methodes abstraites définies dans l'interface et dans les interfaces dont elle hérite.