L'héritage
Exemple :
classe Véhicule //n’importe quel objet roulant
privé:
noveh : entier
couleur : chaîne de caractères
poids : réel //poids du véhicule
public:
PROCEDURE initialiser (données : n : entier, c : chaîne de caractères, p : réel)
DEBUT
noveh ß n
couleur ß c
poids ß p
FIN
FONCTION donnercouleur() : chaîne de caractères
DEBUT
Retourner couleur
FIN
FONCTION donnerpoids() : réel
DEBUT
Retourner poids
FIN
Fclasse
classe Voiture : hérite de Véhicule //classe pour les voitures
Fclasse
Cette déclaration stipule que la classe "Voiture" dérive de "Véhicule", c’est à dire qu’elle en possède toutes les caractéristiques. En clair, une" Voiture " est un "Véhicule". La classe "Véhicule" est dite "classe de base", " Voiture " est appelée une "classe dérivée de Véhicule".
Un objet de type « Voiture » possédera donc les données membres noveh, couleur, poids et toutes les méthodes de « Véhicule » (sauf les constructeurs (cf. codage en Java)) :
procédure initialiser(données: n : entier, c : chaîne de caractères, p : réel)
fonction donnercouleur() : chaîne de caractères
fonction donnerpoids() : réel
Nous venons de voir que la classe dérivée "hérite" des membres définis dans la classe de base (données et méthodes).
Programme Test1
DEBUT
VAR voit1 : Voiture
pds : réel
//initialisation de voit1
voit1.initialiser (1, « rouge », 1500)
Afficher « Couleur : »,
voit1.couleur //interdit tout comme avec
un
//objet de type Véhicule
pds ß voit1.donnerpoids()
…
FIN
Remarque : en Java, l'héritage multiple n'existe pas, seul l'héritage simple est possible, c'est à dire qu'une classe ne peut être dérivée de plusieurs classes.
Pour le moment, la classe "Voiture" est identique à la classe " Véhicule" et ne présente donc aucun intérêt. La spécialisation consiste à ajouter des méthodes ou des données à la classe dérivée (on dit que l’on spécialise la classe de base).
Exemple :
classe Voiture : hérite de Véhicule
privé :
nbpassagers :entier
métallisé : booléen
public :
PROCEDURE initialiser(données : n : entier, c : chaîne de caractères, p : réel, gens : entier, metal : booléen)
DEBUT
initialiser(n, c, p)
nbpassagers ß gens
métallisé ß métal
FIN
FONCTION donnerpassagers() : entier
DEBUT
Retourner nbpassagers
FIN
FONCTION donnercouleur() : chaîne de caractères
DEBUT
SI métallisé
ALORS
texte ß " métallisé"
SINON
texte ß " mat"
FSI
Retourner Véhicule::donnercouleur() + texte // Il est impossible de retourner
// couleur + texte FIN
Fclasse
Chaque « Voiture » possède 5 membres "données" : noveh, couleur, poids, nbpassagers, métallisé et toutes les méthodes des 2 classes :
procédure initialiser(données : n : entier, c : chaîne de caractères, p : réel) //initialisation de no, couleur et poids
procédure initialiser(données : n : entier, c : chaîne de caractères, gens :entier, métal : booléen ) //initialisation de toutes les données
fonction donnercouleur() : chaîne de caractères //fonction de la classe Véhicule
fonction donnercouleur() : chaîne de caractères //fonction de la classe Voiture
fonction donnerpoids() : réel
fonction donnerpassagers() : entier
Il est donc possible d’attribuer le même nom à des membres de la classe héritée et à des membres de la classe de base.
Programme Test2
DEBUT
VAR véh1, véh2 : Véhicule
voit1, voit2 : Voiture
pds : réel
couleur : chaîne de caractères
nbpas : entier
voit1.initialiser (1, "bleu", 1000) // Appel à initialiser de Véhicule
voit2.initialiser (2, "rouge", 1500, 7, Vrai) // Appel à initialiser de Voiture
véh1.initialiser (1, "vert", 800) // Appel à initialiser de Véhicule
véh2.initialiser (2, "jaune", 900, 7,
Vrai)
// Erreur
pds ß voit1.donnerpoids() // Appel à donnerpoids de Véhicule
pds ß voit2.donnerpoids() // Appel à donnerpoids de Véhicule
pds ß véh1.donnerpoids() // Appel à donnerpoids de Véhicule
couleur ß voit1.donnercouleur() // Appel à donnercouleur de Voiture
couleur ß voit2.donnercouleur() // Appel à donnercouleur de Voiture
couleur ß véh1.donnercouleur() // Appel à donnercouleur de Véhicule
nbpas ß voit1.donnerpassagers() // Appel à donnerpassagers
// Peut rendre n'importe quoi car non
// initialisé (c'est ce que l'on considère en
// pseudo-code)
nbpas ß voit2.donnerpassagers() // Appel à donnerpassagers
// Place 7 dans nbpas
nbpas ß
véh1.donnerpassagers()
// Erreur
…
FIN
Remarque : il est impossible d'appliquer une méthode ou d'utiliser une donnée de la classe dérivée avec un objet de la classe de base (on verra qu’on peut le faire en Java avec une référence de la classe de base si la référence désigne un objet de la classe dérivée).
Règles pour les méthodes publiques :
- Si la méthode n’existe pas dans la classe dérivée, il y a appel de la méthode de la classe de base.
Exemple : ………………………………..
- S'il y a une méthode de même nom mais avec une signature différente, l’appel se fait suivant la concordance entre les paramètres effectifs et les paramètres formels. On parle de surcharge comme c'est le cas à l'intérieur d'une même classe.
Exemple : ……………………………..
- Si la signature est la même, on dit alors que la méthode est redéfinie dans la classe dérivéeou qu’elle outrepasse la méthode de la classe de base. La méthode de la classe de l’objet d’appel s’applique, on dit qu’elle cache la méthode de la classe de base. On dit également qu'il y a masquage de la méthode de la classe de base.
Exemple : ……………………………..
Pour appeler explicitement la méthode de la classe de base (ceci sera possible uniquement à l'intérieur de la classe héritée et non dans les autres programmes) avec un objet de la classe dérivée, il faudra indiquer NomClasseBase::NomMéthode().
Exemple : ……………………………..
C’est évidemment la méthode de la classe dérivée qui est considérée quand la méthode n’existe pas dans la classe de base (dans ce cas, Java ne regarde même pas dans la classe de base).
Exemple : ……………………………..
Règles pour les données publiques :
- Si la donnée n'existe pas dans la classe dérivée, c'est la donnée de la classe de base qui est considérée.
- Si la donnée est redéfinie, c'est la donnée de la classe de l'objet d'appel qui est considérée à moins d'indiquer NomClasseBase::NomDonnée.
- C’est évidemment la donnée de la classe dérivée qui est considérée quand la donnée n’existe pas dans la classe de base (dans ce cas, Java ne regarde même pas dans la classe de base).
Règles pour les données et méthodes privées
Les membres privés de la classe de base sont inaccessibles à l'intérieur des méthodes de la classe dérivée comme ils le sont de façon générale à l'extérieur de la classe de base.
Par exemple, il ne sera pas possible de désigner couleur dans une méthode de la classe Voiture car c’est un membre privé de « Véhicule » ; il faudra donc utiliser la fonction donnercouleur de la classe « Véhicule »pour récupérer la valeur de couleur.
Remarque :
Toutes les règles citées précédemment sont également valables pour les méthodes et données statiques.
Considérons les déclarations :
CLASSE A
privé
x, y : entier
public :
PROCEDURE initial (données : i, j : entier)
PROCEDURE affiche ()
PROCEDURE affiche_x ()
FCLASSE
CLASSE B : hérite de A
privé
x, z : entier /* redéfinition de x */
public
PROCEDURE initial (données : i, j, k, l : entier) /* surcharge */
PROCEDURE affiche () /* redéfinition */
FCLASSE
La classe B possède ici quatre membres données de type entier. Les membres x et y sont hérités de la classe A, le membre z est ajouté, le membre x est redéfini. Un membre "donnée" redéfini ne remplace pas le membre correspondant de la classe de base, il est ajouté. Chaque objet de la classe B possédera donc les membres données x, y, z et x de A.
La classe B a accès aux cinq méthodes membres : initial(données : i, j, k, l : entier), initial(données : i, j : entier), affiche(), A::affiche() et affiche_x().
Exercice d'application :
Soient les variables unA et unB respectivement de classe A et B déclarés dans le programme principal. Remplir le tableau suivant :
Instruction ou partie d'instruction |
Localisation |
Membre utilisé ou erreur |
unB.initial (2, 3, 5, 6); |
programme principal (pp) |
|
initial (1, 2) ; |
classe B |
|
unB.initial (1, 2) ; |
pp |
|
x |
classe B |
|
A::x |
classe B |
|
x |
classe A |
|
x |
pp |
|
affiche_x () ; |
classe B |
|
A::affiche_x () ; |
classe B |
|
unB.affiche_x () ; |
pp |
|
affiche () ; |
classe A |
|
affiche () ; |
classe B |
|
A::affiche () ; |
classe B |
|
unB.affiche () ; |
pp |
|
B::affiche () ; |
classe A |
|
z |
classe A |
|
Une classe dérivée peut à son tour être spécialisée et devenir la classe de base d’une autre classe. Par exemple la classe « Deuxroues » peut hériter de « Véhicule » et les classes « Velo » et « Moto » peuvent hériter de « Deuxroues ». Dans ce cas, « Velo » et « Moto » héritent des membres de « Deuxroues » et implicitement des membres de « Véhicule » via « Deuxroues ».
Toutes les classes dérivées peuvent utiliser directement les méthodes publiques (ainsi que les données publiques) des classes ancêtres (ancêtre direct ou non).
Les principes présentés précédemment s’appliquent.
q Ecriture de la classe Véhicule
class Véhicule
{
private int noveh;
private String couleur;
private float poids;
public void initialiser(int n, String c, float p)
{
noveh = n;
couleur=c;
poids=p;
}
public float donnerpoids()
{
return poids;
}
public String donnercouleur()
{
return couleur;
}
}
q Ecriture de la classe Voiture
class Voiture extends Véhicule
{
private int nbpassagers;
private boolean métallisé;
public void initialiser(int n, String c, float p, int gens, boolean
métal)
{
initialiser(n, c, p);
nbpassagers=gens;
métallisé=métal;
}
public int donnerpassagers()
{
return nbpassagers;
}
public String donnercouleur()
{
return super.donnercouleur() + (métallisé ?" métallisé":" mat");
}
}
Les possibilités d'accès sont identiques à ce qui a été vu en algorithmique.
q Ecriture de la classe Test2 (pour tester Voiture et Véhicule)
class Test2
{
public static void main(String [] argv)
{
Véhicule véh1 = new Véhicule(), véh2 = new Véhicule();
Voiture voit1 = new Voiture(), voit2 = new Voiture();
float pds;
String couleur;
int nbpas;
voit1.initialiser (1, "bleu", 1000); // Appel à initialiser de Véhicule
voit2.initialiser (2,"rouge",1500,7,true); // Appel à initialiser de Voiture
véh1.initialiser (1, "vert", 800); // Appel à initialiser de Véhicule
véh2.initialiser (2, "jaune", 900, 7, true); //
Erreur
pds = voit1.donnerpoids(); // Appel à donnerpoids de Véhicule
pds = voit2.donnerpoids(); // Appel à donnerpoids de Véhicule
pds = véh1.donnerpoids(); // Appel à donnerpoids de Véhicule
couleur = voit1.donnercouleur(); // Appel à donnercouleur de Voiture
couleur = voit2.donnercouleur(); // Appel à donnercouleur de Voiture
couleur = véh1.donnercouleur(); // Appel à donnercouleur de Véhicule
nbpas = voit1.donnerpassagers(); // Appel à donnerpassagers
// Place 0 dans nbpas
nbpas = voit2.donnerpassagers(); // Appel à donnerpassagers
// Place 7 dans nbpas
nbpas = véh1.donnerpassagers();
// Erreur
}
}
Nous connaissons déjà les modificateurs de visibilité (c'est à dire les statuts public et private), nous allons découvrir final (final peut s'employer avec un autre modificateur tel private ou public).
Classes finales
Cela signifie que la classe ne pourra pas être héritée. Il ne sera alors pas possible de la dériver.
Exemple :
final class Velo
{
….
}
Toute tentative donnera un message d'erreur à la compilation.
Variables finales
Ceci est utilisé pour déclarer des constantes. Généralement, il s'agira d'une donnée statique car on ne voit pas l'intérêt d'avoir une donnée de même valeur dans chaque objet.
Exemples :
public static final double EURO = 6.5569;
private static final double TVA = 19.6;
On pourra en plus définir une fonction d'accès pour TVA comme on le fait pour une autre donnée privée.
public static double tva()
{
return TVA;
}
Méthodes finales
C'est une méthode qui ne peut être redéfinie. En revanche, elle pourra être surchargée.
Il est impossible de construire un objet d'une classe dérivée avec le constructeur de la classe de base. Par exemple :
Voiture v=new Véhicule(); // Provoque une erreur à la compilation
Ce qui s'explique car le constructeur de Véhicule ne peut en aucun cas initialiser les données spécialisées de Voiture.
q Ecriture de la classe Véhicule2
class Véhicule2
{
private int noveh;
String couleur;
private float poids;
public Véhicule2 (int n, String c, float p)
{
noveh=n;
couleur=c;
poids=p;
}
public float donnerpoids()
{
return poids;
}
public String donnercouleur()
{
return couleur;
}
}
q Ecriture de la classe Voiture2
class Voiture2 extends Véhicule2
{
private int nbpassagers;
private boolean métallisé;
public Voiture2(int n, String c, float p, int gens, boolean métal)
{
super(n, c, p);
nbpassagers=gens;
métallisé = métal;
}
public int donnerpassagers()
{
return nbpassagers;
}
public String donnercouleur()
{
return super.donnercouleur() + (métallisé ?" métallisé":" mat");
}
}
q Ecriture de la classe Test22 (pour tester Voiture2 et Véhicule2)
class Test22
{
public static void main(String [] argv)
{
Véhicule2 véh1 = new Véhicule2(1, "vert", 800);
Véhicule2 véh2 = new Véhicule2(2,"jaune",900,7,true); //
véh2 ne sera
pas
// créé, il y aura erreur
Voiture2 voit1 = new Voiture2(1, "bleu", 1000, 0, false),
voit2 = new Voiture2(2, "rouge", 1500, 7, true);
float pds;
int nbpas;
String couleur;
pds = voit1.donnerpoids(); // Appel à donnerpoids de Véhicule
pds = voit2.donnerpoids(); // Appel à donnerpoids de Véhicule
pds = véh1.donnerpoids(); // Appel à donnerpoids de Véhicule
couleur = voit1.donnercouleur(); // Appel à donnercouleur de Voiture
couleur = voit2.donnercouleur(); // Appel à donnercouleur de Voiture
couleur = véh1.donnercouleur(); // Appel à donnercouleur de Véhicule
nbpas = voit1.donnerpassagers(); // Appel à donnerpassagers
// Place 0 dans nbpas
nbpas = voit2.donnerpassagers(); // Appel à donnerpassagers
// Place 7 dans nbpas
nbpas = véh1.donnerpassagers();
// Erreur
}
}
Remarques :
. A partir du moment où la classe de base ne possède pas de constructeur par défaut, il est impératif de prévoir un constructeur pour la classe dérivée ne serait-ce que pour transmettre
les paramètres nécessaires au constructeur de la classe de base.
Si la classe de base possède un constructeur par défaut (implicite c'est à dire aucun constructeur défini ou bien explicite), la classe dérivée n'est pas obligée de mettre en place un constructeur.
. Si on avait souhaité qu'une voiture puisse être construite avec ou sans paramètres, il aurait fallu prévoir en plus un constructeur sans paramètres pour Voiture2 et pour Véhicule2.
public Véhicule2 ()
{
Code éventuel
}
public Voiture2 ()
{
Code éventuel
}
Exercice d'application sur les constructeurs
Soient les classes A et B :
class A
{
private int x, y;
public A() {} // constructeur par défaut
public A (int i, int j) // constructeur paramétré
{
x = i ;
y = j;
}
}
class B extends A
{
private int x, z;
public B (){}
public B (int i, int j, int k, int l)
{
super(i,j) ; //appel du constructeur de A avec ses paramètres
x = k;
z = l;
}
}
Donner l'objet obtenu (ou erreur) pour chaque instruction de création d’objet :
Instruction |
Objet créé ou erreur |
B unb=new B() ; |
|
B unb=new B(2,3,4,5) ; |
|
B unb =new B(1,2); |
|
Il est tout à fait possible de dériver les classes du JDK.
Exemple : création d'une classe Férié dérivée de la classe GregorianCalendar
q Ecriture de la classe Férié
import java.util.GregorianCalendar;
class Férié extends GregorianCalendar
{
private String nom;
public Férié(String n, int j, int m, int a)
{
super(a, m-1, j);
nom=n;
}
public String rendnom()
{
return nom;
}
}
q Ecriture de la classe Progférié qui utilise la classe Férié
class Progférié
{
public static void main(String [] argv)
{
Férié jourférié = new Férié("Noel",25, 12, 2001);
int jour = jourférié.get(Férié.DAY_OF_MONTH);
int mois = jourférié.get(Férié.MONTH)+1;
int an = jourférié.get(Férié.YEAR);
int quant = jourférié.get(Férié.DAY_OF_YEAR);
System.out.println (jourférié.rendnom() + " "+jour + " " + mois + " " + an);
System.out.println ("Ce sera le " + quant + "eme jour de l'annee");
}
}
Une classe qui n’est utilisée que pour être dérivée peut être définie comme abstraite. Toute instanciation est alors impossible.
Exemple : soit la hiérarchie suivante :
![]() |
![]() |
· Classe Animal :
abstract class Animal
{
String prénom;
public Animal(String p)
{
prénom = p;
}
public String rendprénom()
{
return prénom;
}
abstract public void crier();
}
Toutes les méthodes déclarées abstraites dans la classe abstraite devront être redéfinies (donc { bloc } obligatoire avec bloc éventuellement vide) dans chaque classe dérivée que l'on veut instancier. Si l'une des méthodes abstraites n'est pas redéfinie dans une classe dérivée, cette classe dérivée est elle-même abstraite et doit être déclarée comme telle.
La déclaration d'une méthode abstraite dans une classe rend la classe abstraite et nous oblige à la déclarer comme telle.
Toutes les méthodes non abstraites d'une classe abstraite ne sont pas obligatoirement redéfinies dans les classes dérivées mais peuvent l'être.
· Classe Ovipare :
abstract class Ovipare extends Animal
{
public Ovipare(String p)
{
super(p);
}
abstract public void crier();
}
Un constructeur ne peut pas être abstrait.
Dans cet exemple, on est obligé de définir le constructeur dans les classes dérivées car la classe de base (Animal) ne comprend pas de constructeur par défaut. Mais si on avait
défini :
public Animal(String p)
{
prénom = p;
}
public Animal()
{
}
Alors, il n'y aurait pas obligation de redéfinir le constructeur.
· Classe Oiseau :
class Oiseau extends Ovipare
{
public Oiseau(String p)
{
super(p);
}
public void crier()
{
System.out.println ("Cuicui");
}
}
Ainsi que nous l'avons dit ci-avant, il est obligatoire de redéfinir (donc {bloc} obligatoire avec bloc éventuellement vide) la méthode crier dans les classes que l'on veut instancier.
· Classe Mammifère :
abstract class Mammifère extends Animal
{
String race;
public Mammifère(String p, String r)
{
super(p);
race = r;
}
public String rendrace()
{
return race;
}
abstract public void crier();
}
· Classe Chien :
class Chien extends Mammifère
{
public Chien(String p, String r)
{
super(p, r);
}
public void crier()
{
System.out.println ("Ouah-Ouah");
}
}
· Classe Chat :
class Chat extends Mammifère
{
public Chat(String p, String r)
{
super(p, r);
}
public void crier()
{
System.out.println ("Miaou-Miaou");
}
}
· Classe pour les tests :
class TestAnimaux
{
public static void main(String [] argv)
{
Chien milou = new Chien("Milou", "Berger des Pyrénées");
Chat tigri = new Chat("Tigri", "Angora");
Oiseau nestor = new Oiseau("Nestor");
milou.crier();
tigri.crier();
nestor.crier();
}
}
Remarques :
· Ovipare et Mammifère sont obligées d'être abstraites ou bien de redéfinir (donc {bloc} obligatoire avec bloc éventuellement vide) la méthode crier.
De toute façon :
- On considère que ces 2 classes ne peuvent être instanciées donc on les déclare abstraites.
- On considère que le cri d'un mammifère ou d'un ovipare est une notion abstraite donc on ne peut donner un corps à la méthode crier dans Ovipare et dans Mammifère donc on déclare la méthode crier comme abstraite dans ces 2 classes.
· Une classe peut être déclarée abstraite même si elle ne contient aucune méthode abstraite.
6.5.2. Le polymorphisme
a. Principe
Le fait qu'une référence du type de la classe parente désigne un objet d'une classe dérivée s'appelle le surcasting.
Ex :
Animal monanimal = new Chien("Rantanplan", "Setter");
ou en version longue :
Animal monanimal;
monanimal = new Chien("Rantanplan", "Setter");
Remarque : il est impossible de faire l'opération inverse ainsi que nous l'avons déjà vu, par exemple il est impossible d'écrire :
Chien monanimal = new
Animal("Rantanplan", "Setter");
Le typage (ou liaison) dynamique (ou tardive) consiste à déterminer la méthode à appeler seulement lors de l’exécution en prenant en compte le type effectif de l’objet désigné par le handle et non le type déclaré du handle.
Java fonctionne par typage tardif sauf pour les méthodes déclarées avec le modificateur final.
Nous allons voir un exemple de typage tardif avec la nouvelle version de la classe de test.
· Nouvelle version de la classe de test :
class TestAnimaux2
{
public static void main(String [] argv)
{
Animal milou = new Chien("Milou", "Berger des Pyrénées");
Animal tigri = new Chat("Tigri", "Angora");
Animal nestor = new Oiseau("Nestor");
milou.crier();
tigri.crier();
nestor.crier();
}
}
Ce sont bien les méthodes crier des classes dérivées qui sont appelées au moment de l'exécution. Ceci n'a rien à voir avec le fait que la méthode crier soit abstract.
Attention, il faut que la méthode existe dans la classe Animal (même si ce n'est pas celle qui sera exécutée) car sinon il y a une erreur à la compilation.
Le polymorphisme se définit par le fait que les instances des différentes classes répondent à la même méthode mais d'une façon appropriée à chaque classe. Une même instruction provoquera l'appel de telle ou telle méthode en fonction de la classe de l'objet effectivement désigné par le handle au moment de l'exécution.
Le polymorphisme est possible grâce au système de liaison tardive.
b. Passage de paramètres et polymorphisme
On va voir qu'une référence transmise explicitement en paramètre est également polymorphe.
· Nouvelle version de la classe Animal :
abstract class Animal
{
String prénom;
public Animal(String p)
{
prénom = p;
}
public String rendprénom()
{
return prénom;
}
abstract public void crier();
public void copain(Animal a)
{
System.out.println(rendprénom() + " et " +
a.rendprénom() + " sont copains");
System.out.println("Voici les cris que l'on entend : ");
crier();
a.crier();
}
}
· Nouvelle version de la classe de test :
class TestAnimaux3
{
public static void main(String [] argv)
{
Animal milou = new Chien("Milou", "Berger des Pyrénées");
Animal tigri = new Chat("Tigri", "Angora");
Animal nestor = new Oiseau("Nestor");
milou.copain(tigri); // Provoque l'affichage d'un msg disant que
// Milou et Tigri sont copains puis les cris
// de Milou et de Tigri
milou.crier();
tigri.crier();
nestor.crier();
}
}
On constate que le polymorphisme s'applique pour les 2 appels à crier qui figurent dans la méthode copain.