HERITAGE
Héritage
Introduction:
Pour raccourcir les temps d’écriture et de mise au point
du code d’une application, il est intéressant de pouvoir
réutiliser du code déjà écrit
Réutilisation par des classes clientes
Soit une classe A dont on a le code compilé
Une classe C veut réutiliser la classe A
Elle peut créer des instances de A et leur demander
des services
On dit que la classe C est une classe cliente de la
classe A
Réutilisation avec modifications
Souvent, cependant, on souhaite modifier en
partie le comportement de A avant de le
réutiliser
Le comportement de A convient, sauf pour des détails
qu’on aimerait changer
Ou alors, on aimerait ajouter une nouvelle
fonctionnalité à A
Réutilisation avec modifications
du code source
On peut copier, puis modifier le code source de A
dans des classes A1, A2,...
Problèmes :
– on n'a pas toujours le code source de A
– les améliorations futures du code de A ne seront
pas dans les classes A1, A2,…
Réutilisation par l'héritage
L’héritage existe dans tous les langages objet à
classes
L’héritage permet d’écrire une classe B
– qui se comporte dans les grandes lignes comme la
classe A
– mais avec quelques différences sans toucher au code
source de A
On a seulement besoin du code compilé de A
Réutilisation par l'héritage
Le code source de B ne comporte que ce qui a
changé par rapport au code de A
On peut par exemple
– ajouter de nouvelles méthodes
– modifier certaines méthodes
Vocabulaire
La classe A s'appelle une classe mère, classe
parente ou super-classe
La classe B qui hérite de la classe A s'appelle une
classe fille ou sous-classe
Exemple d’héritage en Java - classe
mère
public class Rectangle {
private int x, y; // point en haut à gauche
private int largeur, hauteur;
// La classe contient des constructeurs,
// des méthodes getX(), setX(int)
// getHauteur(), getLargeur(),
// setHauteur(int), setLargeur(int),
// contient(Point), intersecte(Rectangle)
// translateToi(Vecteur), toString(),...
...
public void dessineToi(Graphics g) {
[Link](x, y, largeur, hauteur);
}
}
Exemple d’héritage en Java -
classe fille
public class RectangleColore extends Rectangle {
private Color couleur; // nouvelle variable
// Constructeurs
...
// Nouvelles Méthodes
public getCouleur() { return [Link]; }
public setCouleur(Color c) { [Link] = c; }
// Méthodes modifiées
public void dessineToi(Graphics g) {
[Link](couleur);
[Link](getX(), getY(),
getLargeur(), getHauteur());
}
}
Code des classes filles
Quand on écrit la classe RectangleColore,
on doit seulement
– écrire le code (variables ou méthodes) lié
aux nouvelles possibilités ; on ajoute ainsi
une variable couleur et les méthodes qui y
sont liées
– redéfinir certaines méthodes ; on redéfinit la
méthode dessineToi()
Redéfinition et surcharge
Ne pas confondre redéfinition et surcharge des
méthodes :
– on redéfinit une méthode quand une nouvelle
méthode a la même signature qu’une méthode héritée de la
classe mère
– on surcharge une méthode quand une nouvelle méthode a le
même nom, mais pas la même signature, qu’une autre méthode
de la même classe
Rappel: Signature d’une méthode (nom de la méthode +
ensemble des types de ses paramètres)
2 façons de voir l’héritage
Particularisation-généralisation :
– un rectangle coloré est un rectangle mais un
rectangle particulier
– la notion de figure géométrique est une
généralisation de la notion de polygone
Une classe fille offre de nouveaux services ou
enrichit les services rendus par une classe : la
classe RectangleColore permet de dessiner
avec des couleurs et pas seulement en « noir et
blanc »
L’héritage en Java
En Java, chaque classe a une et une seule
classe mère (pas d’héritage multiple) dont elle
hérite les variables et les méthodes
Le mot clef extends indique la classe mère :
class RectangleColore extends Rectangle
Par défaut (pas de extends dans la définition
d’une classe), une classe hérite de la classe
Object
Exemples d’héritages
class Voiture extends Vehicule
class Velo extends Vehicule
class VTT extends Velo
class Employe extends Personne
class ImageGIF extends Image
class PointColore extends Point
Exo
Lesquels de ces assertions forment une hiérarchie d’héritage:
Four étend Cuisine
Guitare étend Instrument
Personne étend Employé
Ferrari étend Moteur
Omelette étend Nourriture
Epagneul étend Chien
Contenant étend une Tasse
Métal étend Titanium
GratefulDead étend Groupe
Boisson étend Martini
Astuce: faites le test est-un(e)
Ce que peut faire une classe fille
La classe qui hérite peut
– ajouter des variables, des méthodes et des
constructeurs
– redéfinir des méthodes (exactement les mêmes types
de paramètres)
– surcharger des méthodes (même nom mais pas même
signature) (possible aussi à l’intérieur d’une classe)
Principe important
lié à la notion d’héritage
Si « B extends A », le grand principe est que tout B
est un A
Par exemple, un rectangle coloré est un
rectangle ; un poisson est un animal ; une voiture est un
véhicule
En Java, on évitera d’utiliser l’héritage pour
réutiliser du code dans d’autres conditions
Sous-type
B est un sous-type de A si on peut ranger une expression de
type B dans une variable de type A
Les sous-classes d’une classe A sont des sous types
de A
En effet, si B hérite de A, tout B est un A donc on
peut ranger un B dans une variable de type A
Par exemple,
A a = new B(…); est autorisé
L’héritage en notation UML
1ère instruction d’un constructeur
La première instruction (interdit de placer cet
appel ailleurs !) d’un constructeur peut être un appel
– à un constructeur de la classe mère :
super(...)
– ou à un autre constructeur de la classe :
this(...)
Constructeur de la classe mère
public class Rectangle {
private int x, y, largeur, hauteur;
public Rectangle(int x, int y, int largeur, int hauteur) {
this.x = x;
this.y = y;
[Link] = largeur;
[Link] = longueur;
}
...
}
Constructeurs de la classe fille
public class RectangleColore extends Rectangle {
private Color couleur;
public RectangleColore(int x, int y,int largeur, int hauteur Color couleur) {
super(x, y, largeur, hauteur);
[Link] = couleur;
}
public RectangleColore(int x, int y, int largeur, int hauteur) {
this(x, y, largeur, hauteur, [Link]);
}
...
}
Appel implicite du constructeur
de la classe mère
Si la première instruction d'un constructeur
n'est ni super(...), ni this(...), le compilateur ajoute un
appel implicite au constructeur sans paramètre de la
classe mère (erreur s’il n’existe pas)
=> Un constructeur de la classe mère est
toujours exécuté avant les autres instructions
du constructeur
Toute première instruction
exécutée par un constructeur
Mais la première instruction d’un constructeur
de la classe mère est l’appel à un constructeur
de la classe « grand-mère », et ainsi de suite...
Donc la toute, première instruction qui est
exécutée par un constructeur est le constructeur
(sans paramètre) par défaut de la classe Object !
(D’ailleurs c’est le seul qui sait comment créer un nouvel objet en
mémoire)
Question: Quel est le résultat?
Exemple sur les constructeurs
public class Cercle {
// Constante
public static final double PI = 3.14;
// Variables
private Point centre;
private int rayon;
// Constructeur Ici pas de constructeur sans
public Cercle(Point c, int r) { paramètre
centre = c;
rayon = r; Appel implicite du
} constructeur Object()
// Méthodes
public double surface() {
return PI * rayon * rayon;
}
public Point getCentre() {
return centre;
}
public static void main(String[] args) {
Point p = new Point(1, 2);
Cercle c = new Cercle(p, 5);
[Link]("Surface du cercle:"
+ [Link]());
}
public class CercleColore extends Cercle {
private String couleur;
public CercleColore(Point p, int r, String c) {
super(p, r); Que se passe-t-il si on enlève
cette instruction?
couleur = c;
}
public void setCouleur(String c) {
couleur = c;
}
public String getCouleur() {
return couleur;
}
Une erreur de débutant !
public class CercleColore extends Cercle {
private Point centre;
private int rayon;
private String couleur;
public CercleColore(Point p, int r, String c) {
centre = c;
rayon = r;
couleur = c;
Que se passe-t-il ici?
Héritage – problème d’accès
public class Animal {
String nom; // pas private ; à suivre...
public Animal() {
}
public Animal(String unNom) {
nom = unNom;
}
public void setNom(String unNom) {
nom = unNom;
}
public String toString() {
return "Animal " + nom;
}
}
Héritage – problème d’accès
public class Poisson extends Animal {
private int profondeurMax;
public Poisson(String nom, int uneProfondeur) {
[Link] = nom; // Et si nom est private ?
profondeurMax = uneProfondeur;
}
public void setProfondeurMax(int uneProfondeur) {
profondeurMax = uneProfondeur;
}
public String toString() {
return "Poisson " + nom + " ; plonge jusqu’à "
+ profondeurMax + " mètres";
}
}
Résolution par encapsulation
public class Poisson extends Animal {
private int profondeurMax;
public Poisson(String unNom, int uneProfondeur) {
super(unNom); // convient même si nom est private
profondeurMax = uneProfondeur;
}
public void setProfondeurMax(int uneProfondeur) {
profondeurMax = uneProfondeur;
} nom déclaré en private
public String toString() {
return "Poisson " + getNom()
+ " plonge jusqu’à " + profondeurMax
+ " mètres";
}
De quoi hérite une classe ?
Si une classe B hérite de A (B extends A), elle
hérite automatiquement et implicitement de
tous les membres de la classe A (mais pas des
constructeurs)
Cependant la classe B peut ne pas avoir accès à
certains membres dont elle a implicitement
hérité de A (par exemple, les membres private)
Ces membres sont utilisés pour le bon
fonctionnement de B, mais B ne peut pas les
nommer ni les utiliser explicitement
Redéfinition des attributs
Soit une classe B qui hérite d'une classe A
Dans une méthode d'instance m() de B,
super. sert à désigner un membre de A :
– en particulier, super.m()désigne la méthode de
A qui est donc en train d’être redéfinie dans B :
int m(int i) {
return 500 + super.m(i);
}
On ne peut trouver super.m() dans une méthode
static m() ;
une méthode static ne peut être redéfinie
super.i désigne la variable cachée i de la classe
mère (ne devrait jamais arriver) ; dans ce cas,
super.i est équivalent à ((A)this).i
Exemple
Accès protected
protected joue sur l’accessibilité des
membres (variables ou méthodes) par les classes filles
Un membre protected de la classe A peut être
manipulé par les classes filles de A sans que les
autres classes non filles de A ne puisse les
manipuler
Exemple
public class Animal {
protected String nom;
...
}
public class Poisson extends Animal {
private int profondeurMax;
public Poisson(String unNom, int uneProfondeur) {
nom = unNom; // utilisation de nom de la classe mère
profondeurMax = uneProfondeur;
}
Protected autorise également l’utilisation par les
classes du même paquetage que la classe où est
définie le membre ou le constructeur
Du code de B peut accéder à un membre protected
de A (méthode m() ci-dessous) dans une instance de
B ou d’une sous-classe C de B mais pas d’une
instance d’une autre classe (par exemple, de la
classe est A ou d’une autre classe fille D de A) ; voici
du code de la classe B :
A a = new A(); // A classe mère de B
B b = new B();
C c = new C(); // C sous-classe de B
D d = new D(); // D autre classe de A
a.m(); // interdit
b.m(); // autorisé
c.m(); // autorisé
d.m(); // interdit
Attention, protected joue donc sur
l’accessibilité par B du membre m hérité (B comme
sous-classe de A)
mais pas sur l’accessibilité par B du membre m des
instances de A (B comme cliente de A)
Polymorphisme
Contexte: soit B hérite de A et que la méthode
m() de A soit redéfinie dans B
Quelle méthode m() sera exécutée dans le code
suivant, celle de A ou celle de B ?
A a = new B(5);
a.m(); a est un objet de la classe B
mais il est déclaré de la classe A
La méthode appelée ne dépend que du type réel
(B) de l’objet a (et pas du type déclaré, ici A).
C’est la méthode de la classe B qui sera exécutée
Le polymorphisme est le fait qu’une même écriture peut
correspondre à différents appels de méthodes ; par exemple,
A a = x.f();
a.m();
appelle la méthode m() de A ou de n’importe quelle sous-
classe de A f peut renvoyer une instance de A
ou de
n’importe quelle sous-classe de A
Ce concept est une notion fondamentale de la
programmation objet, indispensable pour une
utilisation efficace de l’héritage
Exemple 1:
Animal[] animaux=new Animal[5]
Animaux[0]= new Chien(); Déclare un tableau de type
Animaux[1]= new Chat(); animal
Animaux[2]= new Loup();
Animaux[3]= new Hippo(); On peut placer un objet de n’importe
Animaux[4]= new Lion(); quelle sous-classe d’Animal dans le
tableau
for (int i=0;i<[Link];i++) {
Animaux[i].manger();
Animaux[i].vagonbonder();
}
Class Veto {
Public void soigner(Animal a) {
[Link]();
}
Class ProprietaireDAnimaux {
Public void start() {
Veto v = new Veto();
Chien c = new Chien();
Hippo h = new Hippo();
[Link](c);
[Link](h);
}
Remarques:
Le type de la référence peut être la superclasse du
type de l’objet réel
Les arguments et les types de retour peuvent
également être polymorphes
Mécanisme du polymorphisme
Le polymorphisme est obtenu grâce au « late
binding » (liaison retardée) : la méthode qui
sera exécutée est déterminée
– seulement à l’exécution, et pas dès la
compilation
– par le type réel de l’objet qui reçoit le message (et
pas par son type déclaré)
Exemple 2
public class Figure {
public void dessineToi() { }
}
public class Rectangle extends Figure {
public void dessineToi() {
...
}
public class Cercle extends Figure {
public void dessineToi() {
...
}
public class Dessin { // dessin composé de plusieurs figures
private Figure[] figures;
...
public void afficheToi() {
for (int i=0; i < nbFigures; i++)
figures[i].dessineToi();
}
public static void main(String[] args) {
Dessin dessin = new Dessin(30);
. . . // création des points centre, p1, p2
[Link](new Cercle(centre, rayon));
[Link](new Rectangle(p1, p2));
[Link]();
…
Typage statique et polymorphisme
En Java, le typage statique doit garantir dès la
compilation l’existence de la méthode appelée :
la classe déclarée de l’objet qui reçoit le message doit
posséder cette méthode
Ainsi, la classe Figure doit posséder une méthode
dessineToi(), sinon, le compilateur refusera de
compiler l’instruction
« figures[i].dessineToi() »
Utilisation du polymorphisme
Bien utilisé, le polymorphisme évite les codes qui comportent de nombreux
embranchements et tests ;
sans polymorphisme, la méthode dessineToi() aurait dû s’écrire :
for (int i=0; i < [Link]; i++) {
if (figures[i] instanceof Rectangle) {
. . . // dessin d’un rectangle
}
else if (figures[i] instanceof Cercle) {
. . . // dessin d’un cercle
}
}
Utilisation du polymorphisme
Le polymorphisme facilite l’extension des programmes :
on peut créer de nouvelles sous-classes sans toucher aux programmes
déjà écrits
Par exemple, si on ajoute une classe Losange, le code de
afficheToi() sera toujours valable
Sans polymorphisme, il aurait fallu modifier le code source de
la classe Dessin pour ajouter un nouveau test :
if (figures[i] instanceof Losange) {
… // dessin d’un losange
Extensibilité
Avec la programmation objet, une application est
dite extensible si on peut étendre ses fonctionnalités
sans toucher au code source déjà écrit
C’est possible en utilisant en particulier le
polymorphisme comme on vient de le voir
Mécanisme de la liaison retardée
Soit C la classe réelle d’un objet o à qui on envoie un message
« o.m() »
Si le code de la classe C contient la définition (ou la
redéfinition) d’une méthode m(), c’est cette méthode qui sera
exécutée
Sinon, la recherche de la méthode m() se poursuit dans la
classe mère de C, puis dans la classe mère de cette classe
mère, et ainsi de suite, jusqu’à trouver la définition d’une
méthode m() qui est alors exécutée
Transtypage (cast)
Définitions:
Classe (ou type) réelle d’un objet : classe du
constructeur qui a créé l’objet
Type déclaré d’un objet : type donné au
moment de la déclaration
– de la variable qui contient l’objet,
– ou du type retour de la méthode qui a
renvoyé l’objet
Cast : conversions de classes
Le « cast » est le fait de forcer le compilateur à considérer un
objet comme étant d’un type qui n’est pas le type déclaré ou
réel de l’objet
En Java, les seuls casts autorisés entre classes
sont les casts entre classe mère et classes filles
On parle de upcast et de downcast suivant le fait que le type est
forcé de la classe fille vers la classe mère ou inversement
Syntaxe
Pour caster un objet en classe C :
(C) o;
Exemple :
Velo v = new Velo();
Vehicule v2 = (Vehicule) v;
UpCast : classe fille → classe mère
Upcast : un objet est considéré comme une
instance d’une des classes ancêtres de sa classe
réelle
Il est toujours possible de faire un upcast : à
cause de la relation est-un de l’héritage, tout
objet peut être considéré comme une instance
d’une classe ancêtre
Le upcast est souvent implicite
Utilisation du UpCast
Il est souvent utilisé pour profiter ensuite du
polymorphisme :
Figure[] figures = new Figure[10];
figures[0] = new Cercle(p1, 15);
...
figures[i].dessineToi();
DownCast : classe mère → classe
fille
Downcast : un objet est considéré comme étant d’une
classe fille de sa classe de déclaration
Toujours accepté par le compilateur
Mais peut provoquer une erreur à l’exécution ;à
l’exécution il sera vérifié que l’objet est bien de la
classe fille
Un downcast doit toujours être explicite
Utilisation du DownCast
Utilisé pour appeler une méthode de la classe fille
qui n’existe pas dans une classe ancêtre
Figure f1 = new Cercle(p, 10);
. ..
Point p1 = ((Cercle)f1).getCentre();
Downcast pour récupérer les
éléments d’une liste
// Ajoute des figures dans un ArrayList.
// Un ArrayList contient un nombre quelconque
// d’instances de Object
ArrayList figures = new ArrayList();
[Link](new Cercle(centre, rayon));
[Link](new Rectangle(p1, p2));
...
// get() renvoie un Object. Cast nécessaire pour appeler
// dessineToi() qui n’est pas une méthode de Object
((Figure)[Link](i)).dessineToi();
...
Classe final (et autres au final)
Classe final : ne peut avoir de classes filles
(String est final)
Méthode final : ne peut être redéfinie
Variable (locale ou d’état) final : la valeur ne
pourra être modifiée après son initialisation
Paramètre final (d’une méthode ou d’un catch) : la
valeur (éventuellement une référence) ne pourra être
modifiée dans le code de la méthode
Exo
Créez une classe Liquide contenant seulement une méthode
imprimer() qui affiche : "je suis un liquide".
Créez 2 classes dérivées de la classe Liquide, les classes
Cafe et Lait, dont les méthodes imprimer() affichent
respectivement "je suis un Café", "je suis du Lait".
Enfin vous créerez une classe Tasse ayant un attribut l de la
classe Liquide et une méthode
AjouterLiquide (Liquide li){
l=li ;
[Link]() ;
}
Testez l'utilisation.
EXO: Créez la classe Vehicule qui contient les données
private boolean moteur, private int vitesseMax
et les méthodes suivantes :
constructeur par défaut+autres qui initialise la vitesse et le moteur à vrai,
String toString(), qui renvoie les caractéristiques du véhicule,
void vmax() qui affiche la vitesse maximale du véhicule.
Créez ensuite une classe Voiture_Composee dont les membres sont
private Vehicule ve,
private int nombreDePortes,
et Voiture_Derivee (fille de la classe Vehicule) avec la donnée membre
private int nombreDePortes.
Pour ces deux dernières classes, définissez leur constructeur et méthode toString.
Comparez les deux implantations. Comment accède-t-on aux différentes
données membres et aux différentes méthodes des classes Voiture_Composee et
Voiture_Derivee ? Affichez la vitesse maximale de la voiture dans les deux cas.