• Aucun résultat trouvé

Problématique de l'héritage (1/4)

N/A
N/A
Protected

Academic year: 2021

Partager "Problématique de l'héritage (1/4)"

Copied!
31
0
0

Texte intégral

(1)

Problème de réutilisabilité : comment écrire une classe qui possède tous les membres (attributs ou méthodes) d'une classe et d'autres en plus, sans tout réécrire?

Problématique de l'héritage (1/4)

class Salarie{

String nom;

int id;

Contrat c;

float salaire_fixe;

void obtenirAugmentation(float f)...

void changerContrat(Contrat c)...

}

class Commercial{

String nom;

int id;

Contrat c;

float salaire_fixe;

float prime;

void obtenirAugmentation(float f)...

void changerContrat(Contrat c)...

float ventesDuMois() ...

}

(2)

Problème de généralisation : comment éviter d'écrire plusieurs fois la même chose dans des classes qui ont des membres communs mais pas tous?

Problématique de l'héritage (2/4)

class CadreDirigeant{

String nom;

int id;

Contrat c;

float salaire_fixe;

float stockOptions;

void obtenirAugmentation(float f)...

void changerContrat(Contrat c)...

void parachuteDore(float f) ...

}

class Commercial{

String nom;

int id;

Contrat c;

float salaire_fixe;

float prime;

void obtenirAugmentation(float f)...

void changerContrat(Contrat c)...

float ventesDuMois() ...

}

(3)

Solution non réutilisable, difficilement testable et peu maintenable : la bidouille.

Problématique de l'héritage (3/4)

class Salarie{

String nom;

int id;

Contrat c;

float salaire_fixe;

float primeOuStockoptions;

int type; // 0:commercial, 1:cadredirigeant, 2:technicien

void obtenirAugmentation(float f)...

void changerContrat(Contrat c)...

float ventesDuMois(){

if(type != 0) return 0;

else ...

}

void parachuteDore(float f){

if(type != 1) return 0;

else ...

}

(4)

Solution conforme aux principes du génie logiciel : l'héritage.

Une classe B hérite d'une classe A signifie que tout membre de A (sauf les constructeurs) est aussi membre de B.

Problématique de l'héritage (4/4)

class Salarie{

String nom;

int id;

Contrat c;

float salaire_fixe;

void obtenirAugmentation(float f)...

void changerContrat(Contrat c)...

}

class CadreDirigeant extends Salarie{

float stockOptions;

void parachuteDore(float f) ...

} class Commercial extends Salarie{

float prime;

float ventesDuMois() ...

}

(5)

Si B hérite de A, A est super-classe de B et B est sous-classe de A.

Il s'agit d'une inclusion ensembliste : toute instance de B est instance de A.

L'héritage est transitif : si B hérite de A et C hérite de B, alors C hérite de A.

Héritage (1/4)

class A{

...

}

class B extends A{

...

}

class A

class B objet o1

objet o2

objet o5 objet o4

objet o3

(6)

Héritage (2/4)

objet o1 instance de Salarie nom : "Toto"

id : 3 c : ...

salaire_fixe : 2000,00

void obtenirAugmentation(float f)...

void changerContrat(Contrat c)...

objet o2 instance de Commercial (et donc aussi instance de Salarie) nom : "Titi"

id : 5 c : ...

salaire_fixe : 2500,00 prime : 500,00

void obtenirAugmentation(float f)...

void changerContrat(Contrat c)...

float ventesDuMois() ...

objet o3 instance de CadreDirigeant (et donc aussi instance de Salarie) nom : "Tutu"

id : 21 c : ...

salaire_fixe : 5000,00 stockOptions : 50000,00

void obtenirAugmentation(float f)...

void changerContrat(Contrat c)...

void parachuteDore(float f) ...

(7)

L'héritage est une généralisation, une relation sorte-de (ou est-un) entre classes.

Attention : héritage et composition sont deux notions bien distinctes.

Ex.: un chat est composé de quatre pattes, mais il est une sorte d'animal

Attention : héritage et abstraction sont deux notions bien distinctes.

On pourrait imaginer un héritage entre objets (cet héritage devrait respecter celui des classes dont ils sont instances).

Héritage (3/4)

class Salarie

objet salarie1 objet salarie2

class Commercial hérite-de

instance-de instance-de

(8)

Héritage (4/4)

class Salarie{

...

}

class CadreDirigeant extends Salarie{

...

} class Commercial extends Salarie{

...

}

class TechnicoCommercial extends Commercial{

...

}

class ResponsableDesAchats extends Commercial{

...

} class VRP extends

Commercial{

...

}

class VRPdépartemental extends VRP{

...

class VRPinternational extends VRP{

...

Un programme n'est jamais trop modulaire, il ne faut pas hésiter à créer de nombreuses classes, structurées horizontalement mais aussi verticalement.

(9)

Le mot-clé super permet d'accéder aux membres de la super-classe.

Par défaut, le préfixage est this. Le préfixage des membres n'est nécessaire qu'en cas d'ambiguité.

super.super est interdit afin d'éviter de passer « par dessus » une classe lors d'un appel de constructeur.

Accès aux membres des super-classes

class Quadrupède{

int nbPattes = 4;

void marche(int vitesse)...

}

class Cheval extends Quadrupède{

void galope(){

super.marche(30);

}

void trotte(){

marche(15);

}

void vaAuPas(){

this.marche(5);

}

boolean peutGaloper(){

return super.nbPattes == 4;

} }

(10)

Lors de la construction d'un objet, le constructeur de la super classe est d'abord appelé (automatiquement ou non), puis celui de la classe.

L'objet n'est totalement créé qu'une fois la chaine des constructeurs redescendue : this n'existe donc pas tant que l'enchainement des appels de constructeurs n'est pas terminé.

Héritage et constructeur (1/3)

class A{

int i;

A(int i){

this.i = i;

} }

class B extends A{

int j;

B(int j){

this.j = j;

} }

class C extends B{

int k;

C(int k){

this.k = k;

} }

(11)

Un appel explicite au super-constructeur est forcément la première instruction du constructeur.

Un appel au super-constructeur peut être implicite ou se faire dans un autre constructeur qui est appelé.

L'appel doit être la première ligne du constructeur : on ne peut donc pas enchainer super() et this()

Héritage et constructeur (2/3)

class B extends A{

int j;

B(int i, int j){

super(i);

this.j = j;

} }

class B extends A{

int j;

B(int i){

super(i);

this.j = 0;

}

B(int i, int j){

this(i);

this.j = j;

} }

(12)

Si dans le constructeur de la sous-classe le constructeur de la super-classe n'est pas explicitement appelé, le compilateur cherche dans la super-classe un constructeur sans paramètre (constructeur par défaut éventuellement).

S'il n'en trouve pas, il refuse de compiler.

Si la sous-classe ne définit aucun constructeur, la super-classe doit avoir un constructeur sans paramètre.

Conclusion : il est préférable de toujours définir explicitement un constructeur sans paramètre.

Héritage et constructeur (3/3)

(13)

Si la classe B hérite de la classe A, tout objet instance de B est instance de A.

Le transtypage ascendant (upcasting) : il consiste à manipuler un objet comme instance d'une classe dont hérite (directement ou non) la classe qui a créé l'objet.

Transtypage implicite :

Transtypage explicite : on transtype (cast) l'objet en le faisant précéder du nom de la super classe entre parenthèses.

Transtypage (1/2)

B toto = new B();

A titi = toto;

B toto = new B();

((A) toto).m();

(14)

Transtypage descendant (downcasting) : il consiste à manipuler un objet comme instance d'une classe qui hérite (directement ou non) de celle qui a créé l'objet.

Le transtypage descendant est forcément explicite.

Pour que le transtypage descendant fonctionne à l'exécution, il faut que l'objet soit bien du type de la classe dans lequel on le transtype.

On peut tester sa classe réelle à l'aide du mot clé instanceof

Transtypage (2/2)

A a = ...;

B b = (B) a;

A a = new B();

if(a instanceof B){

B b = (B) a;

}

(15)

L'API Java offre à la fois des types primitifs (byte, short, int, long, float, double, boolean, char) et les classes correspondantes (Byte, Short,

Integer, Long, Float, Double, Boolean, Character).

Il est possible d'affecter, sans transtypage, une valeur de type primitif à une variable de type la classe correspondante, et vice-versa.

Autoboxing

int i = 2;

Integer j = i;

i = j;

(16)

Principe de substitution de Barbara Liskov : une sous-classe doit pouvoir être utilisée dans toutes les situations où sa classe parente peut l'être, sans qu'il puisse être possible de percevoir une différence.

Ce principe de bonne programmation est violé par la redéfinition d'attribut ou de méthode.

Principe de Liskov

class Chien{

...

public String cri(){

return "ouahouah";

} }

class Caniche extends Chien{

...

public String cri(){

return "wifwif";

} }

(17)

Polymorphisme

B b = new B();

b.toto();

b.i;

A a = new B();

a.toto();

a.i;

class A{

int i = 0;

String s;

void toto()...

void toto(String chaine) ...

}

class B extends A{

int i = 1;

float s, toto;

void toto() ...

int toto(String chaine) ...

}

objet o instance de B (et donc de A) i (de A) : 0

i (de B) : 1

s (de A) : "Truc"

s (de B) : 12,5 toto (de B) : 56,4

void toto() (de A) ...

void toto (String chaine) (de A) ...

La définition dans une sous-classe d'attributs ou de méthodes portant les mêmes noms que des attributs ou méthodes de la super-classe crée des ambiguités au niveau de l'appel de ces attributs et méthodes.

(18)

Masquage d'attribut : la sous classe rédéfinit un attribut avec le même nom qu'un attribut défini dans la super-classe (ou plus haut). Le type n'importe pas.

L'attribut de la classe mère n'est pas écrasé par celui de la classe fille, mais masqué

Il y a ambiguité si l'objet de type B est stocké dans une variable de type A.

Masquage d'attribut (1/2)

class A{

int i = 0;

...

}

class B extends A{

String i = "a";

...

}

B b = new B();

System.out.println(b.i); // affiche a

A a = new B();

a.i ?

(19)

En Java, le polymorphisme d'attribut est résolu par liaison statique : le type des objets est vérifié à la compilation.

L'attribut considéré est donc celui de la classe de déclaration de la variable contenant l'objet.

En C++, la résolution du polymorphisme d'attribut est également réalisé par liaison statique.

Masquage d'attribut (2/2)

A a = new B();

a.i // vaut 0

(20)

Surcharge de méthode (overloading ou polymorphisme ad hoc) : on définit dans une même classe ou dans des classes différentes des méthodes ayant le même nom mais pas les mêmes signatures (paramètres différents).

Les types des paramètres passés lors de l'appel des méthodes suffisent pour lever les ambiguités si les signatures sont incompatibles.

Surcharge de méthode (1/2)

class Parser{

...

int parseFile(String url){

...

}

boolean parseFile(String name,String directory){

...

} }

class TextParser extends Parser{

...

int parseFile(InputStream s){

...

} }

(21)

En cas d'ambiguité lors de l'appel d'une méthode surchargée, le principe de liaison statique est appliqué aux paramètres : le type des valeurs des paramètres est donné par celui des variables qui les contiennent.

Si une valeur de paramètre n'est pas stockée dans une variable, c'est le type de son constructeur qui compte :

Surcharge de méthode (2/2)

class Parser{

int parseFile(InputStream s){

...

} }

class TextParser extends Parser{

int parseFile(TextInputStream s){

...

} }

class TextInputStream extends InputStream{...}

Parser tp = new TextParser();

InputStream is = new TextInputStream("machin.truc");

tp.parseFile(is);

tp.parseFile(new TextInputStream("machin.truc"));

(22)

Redéfinition de méthode (overriding) : on définit dans une classe une méthode ayant le même nom et la même signature qu'une méthode de la super-classe.

En Java, le polymorphisme de méthode est résolu par liaison dynamique (à l'exécution) : la méthode considérée est celle du type réel (type du construteur) de l'objet sur lequel la méthode est appelée.

Redéfinition de méthode (1/2)

class Parser{

int parseFile(InputStream s){

...

} }

class TextParser extends Parser{

int parseFile(InputStream s){

...

} }

Parser tp = new TextParser();

InputStream is = new TextInputStream("machin.truc");

tp.parseFile(is);

(23)

Si le type réel de l'objet ne contient pas la méthode appelée, on remonte dans la hiérarchie des classes pour trouver la méthode.

En C++, le redéfinition de méthode est résolue de façon statique, mais on peut imposer la liaison dynamique en qualifiant une méthode de virtuelle.

Redéfinition de méthode (2/2)

class A{

int toto(int i){

...

} }

class B extends A{

int toto(int i){

...

} }

class C extends B{

}

A c = new C();

c.toto(3);

(24)

On parle aussi de surcharge de méthode lorsque les noms et signatures des méthodes sont les mêmes mais qu'elles sont définies dans des classes non liées par héritage.

Le polymorphisme ad hoc est surtout utile pour uniformiser les traitements d'objets de classes non liées par héritage.

Polymorphisme ad-hoc

class Carré{

void afficher(Graphics g){

...

} ...

}

class Triangle{

void afficher(Graphics g){

...

} ...

}

class Image{

void afficher(Graphics g){

...

} ...

}

MachinGraphique[] mg = ...

...

for(int i = 0;i<mg.length;i++){

mg[i].afficher(mongraphique);

}

(25)

La covariance consiste à redéfinir une méthode en changeant son type de retour uniquement.

Pour qu'il y ait redéfinition de méthode, le type de retour de la méthode redéfinie doit être un sous-type du type de retour de la méthode de départ. Sinon il y a surchage de méthode.

Covariance

class Chef{

Salarie souffreDouleur(){

...

} }

class ChefAtelier extends Chef{

Ouvrier souffreDouleur(){

...

} }

(26)

Certains langages utilisent le typage dynamique : le type des objets n'est connu qu'à l'exécution (Smalltalk, Python, PHP).

→ la résolution du polymorphisme est alors toujours dynamique

Dans d'autres langages, les types sont spécifiés par le programmeur (Java, C++, C#), ou déduits du contexte à la compilation (OCaml, Haskell).

→ la résolution du polymorphisme peut être dynamique ou statique

En C++, le polymorphisme de méthode est résolu par liaison statique. Pour que la résolution du polymorphisme soit dynamique, il faut utiliser des méthodes

« virtuelles ».

Le polymorphisme ne doit être utilisé que lorsqu'il répond à une réelle exigence de représentation des données.

Résolution du polymorphisme

(27)

Un attribut final (const en C++) est une constante (sa valeur est fixée par la première affectation). Par convention, un attribut final est écrit en majuscule.

Un attribut final peut cependant être masqué. On peut contourner ce problème en remplaçant l'attribut par un “faux” accesseur impossible à redéfinir.

Le mot clé final sur une méthode empêche qu'elle soit redéfinie dans une sous- classe.

Contrôle de l'héritage (1/2)

class Insecte{

final int NB_PATTES = 6;

...

}

class Insecte{

final int getNB_PATTES(){

return 6;

} ...

}

(28)

On peut interdire de spécialiser une classe en la déclarant final.

Le compilateur et la machine virtuelle peuvent optimiser l'utilisation de telles classes en évitant la gestion du polymorphisme.

Une bonne partie des classes de l'API Java sont final : les types de base (String, Integer, Long, Float, etc), des classes système (System, Console, etc), des classes réseaux (URI, IDN, etc), des classes pour la sécurité (AccessControler, Permissions, etc), …

Contrôle de l'héritage (2/2)

final class Maïs{

...

}

class MaïsTransgénique extends Maïs{

...

}

(29)

En Java, toutes les classes héritent de la classe Object (même si on ne le précise pas dans la déclaration de la classe!).

Classe Object (1/2)

public class Object{

public Object(){...}

protected Object clone(){...}

public boolean equals(Object o){...}

protected void finalize(){...}

public Class getClass(){...}

public int hashCode(){...}

public String toString(){...}

public void notify(){...}

public void notifyAll(){...}

public void wait(){...}

public void wait(long timeout){...}

public void wait(long timeout, int nanos){...}

}

(30)

Par défaut, l'appel de la méthode o1.equals(o2) renvoie vrai si les objets o1 et o2 ont la même adresse mémoire dans le JVM.

Par défaut, l'appel o.toString() renvoie l'adresse de l'objet o dans la JVM.

Ces méthodes sont destinées à être redéfinies :

Classe Object (2/2)

public class Carre{

int hauteur;

int largeur;

public boolean equals(){

return (this.hauteur == ((Carre) o).hauteur) &&

(this.largeur == ((Carre) o).largeur);

}

public String toString(){

return "Ceci est un carré de hauteur " + hauteur +

" et de largeur " + largeur;

} }

(31)

Héritage multiple

class Truc extends Chose, extends Machin{

Truc(){

this.b; // ??

} } class Machin{

Bidule b;

}

class Chose{

Bidule b;

}

En Java (et Smalltalk, C#, etc), une classe ne peut hériter que d'une seule autre classe (en plus de Object).

Certains langages (C++, Python, Eiffel, etc) permettent l'héritage multiple, qui complique la résolution du polymorphisme.

En C++, le polymorphisme est résolu par liaison statique, mais on peut forcer la liaison dynamique sur les méthodes « virtuelles ».

Références

Documents relatifs

Par conséquent, le triangle BUT est rectangle en U, et d’après la propriété n° 1 du cours, le centre du cercle circonscrit est le milieu de l’hypoténuse [BT].. Construire

Le quadrilatère ABCD possède deux diagonales qui se coupent en leur milieu et ont la même longueur, c’est donc un rectangle.. Exercice n° 7 (……/3) à faire sur

6- Ecrire la méthode void avancerAvecConflit(int n, Pion pio), qui fait la même chose que la méthodes avancer de la classe Pion et qui prend compte de cette modification (la

Polymorphisme ad hoc/de surcharge La possibilité d’avoir plusieurs fonctions ou méthodes portant le même nom, se différenciant par leur arguments (voire leur valeur de retour,

• const C::`vftable' qui fournit l'implémentation de la fonction redéfinie GetInt() de la classe A et utilise la fonction de base SetInt() de la classe A,.. • const C1::`vftable'

En fonction de leurs compétences, confer leurs fiches de poste, d’habilitation et matrice des compétences ils sont répartis sur 10 postes de travail :. Poste techniques diverses

On se propose de vérifier que la fréquence de rotation du moteur Nm = 2150 Tr/mn est compatible avec la vitesse linéaire limite de la boite sur le tapis (V B ) de 0.5 m/s imposée

Le terrain de football du village de Florent est un rectangle de dimensions 110 m sur 80 m. Pour s’échauffer, l’entraineur de Florent demande aux joueurs de faire un sprint sur