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.
|