Dans beaucoup de situations, une classe A a besoin d’exploiter d’autres classes B pour fonctionner correctement. Pour cela, la programmation objet fournit essentiellement deux mécanismes :
la relation entre A et B correspond au fait que A est un « sur-ensemble » de B, un B particulier. Par exemple, un SUV est un Vehicule particulier. Donc SUV devrait hériter de Vehicule.
Définition
L’héritage est une relation « est-un ».
L’héritage est donc un « contrat » : puisque A est un B particulier, toute méthode qui s’applique sur B doit également être applicable à A.
ici, A n’est pas un B particulier. En revanche, il est composé d’un ou plusieurs B. Par exemple, un SUV n’est pas un système de diagnostic (Diagnostic) mais il en contient un. Dans ce cas, on exploite la composition/l’agrégation : la classe SUV contient alors un attribut diag de type Diagnostic.
Définition
La composition/agrégation est une relation « a-un ».
Les méthodes s’appliquant sur B n’ont aucune raison de s’appliquer à A.
Différence entre composition et agrégation : dans la composition, l’attribut B ne peut pas exister indépendamment de son parent A. C’est le cas du système de diagnostic : il est dans une voiture et cela n’aurait pas de sens qu’il existe en dehors. On peut voir cela également de la manière suivante : lorsque l’on détruit A, on devrait détruire B également. À l’inverse, dans l’agrégation, B peut exister indépendamment de A. Par exemple, un SUV peut avoir des passagers. Si l’on détruit le SUV, on ne doit pas détruire ses passagers avec.
En C++, la composition se traduit par le fait que, dans son destructeur, la classe A détruit ses attributs. C’est ce que fait, par exemple, la classe vector. En revanche, dans l’agrégation, elle ne doit pas le faire. Pour cela, en général, soit la classe A ne contient qu’un pointeur vers une instance de B, soit elle contient un wrapper de ce pointeur comme les shared_ptr de la librairie standard.
Soit deux classes Rectangle et Carre. On se pose la question : est-ce que le Rectangle hérite du Carre, ou bien l’inverse ou bien ni l’un ni l’autre ?
En mathématiques, un carré est un rectangle dont la longueur est égale à la largeur. Cela suggère, en informatique, que Carre hérite de Rectangle. Dans ce cas, lorsque l’on construit un Carre, il suffit d’imposer dans le constructeur de Carre que la largeur du carré soit égale à sa hauteur et le tour est joué… Mais, comme on a vu plus haut, avec l’héritage, on a un contrat : tout ce qui s’applique à un Rectangle doit également s’appliquer à un Carre. En particulier, la méthode suivante d’une classe OutilsGeometriques qui n’a aucune relation avec Rectangle ou Carre, doit s’appliquer :
public void applatit (Rectangle rect) {
rect.largeur *= 2;
}
L’objectif de cette méthode est clairement de modifier la largeur sans toucher à la longueur. Elle ne peut donc pas s’appliquer à un Carre. On n’a donc pas respecté le contrat.
En informatique, `Carre` n’hérite donc pas de `Rectangle`.
Peut-être que c’est Rectangle qui hérite de Carre. En effet, dans le carré, on a stocké une des dimensions du rectangle, disons sa longueur, et le rectangle fournit juste un attribut supplémentaire : la largeur. Mais, dans ce cas, toute méthode s’appliquant sur un Carre doit s’appliquer à un Rectangle et, en particulier, la méthode suivante de la classe OutilsGeometriques :
public double surface (Carre carre) {
return carre.longueur * carre.longueur;
}
Clairement, cette méthode renvoie un résultat erroné pour un Rectangle car la formule à appliquer sur les rectangles est différente de celle à appliquer sur les carrés. Donc:
En informatique, `Rectangle` n’hérite pas de `Carre`.
Attention
Ni Rectangle ni Carre n’héritent l’un de l’autre. Ce qui importe, c’est que l’enfant respecte le contrat imposé par son parent : toute méthode s’appliquant sur le parent doit également être applicable à l’enfant, sans modifier le code de la méthode.
Ci-dessous un exemple de composition / agrégation. On ne peut voir la différence quand on déclare les attributs (lignes 8 et 9). En revanche, le prototype du constructeur de la ligne 11 fournit un indice : on passe en argument des passagers déjà construits, ce qui incite à penser qu’il pourrait s’agir d’agrégation, même si ce n’est pas forcément vrai dans tous les cas. Par exemple, les conteneurs comme les vector du C++ recopient les éléments qu’on insère dedans via des push et ils font donc de la composition. En revanche, pour le moteur, on ne passe pas une instance de Moteur. Donc, dans ce cas, on aura de la composition.
1package fr.polytech.aco.vehicule.voiture;
2
3import fr.polytech.aco.Passager;
4import fr.polytech.aco.vehicule.Moteur;
5
6public class SUV {
7 String nom;
8 Moteur moteur; // composition
9 Passager passagers[]; // agrégation
10
11 public SUV(String type_moteur, Passager passagers[]) {
12 // composition => on doit créer un nouveau moteur
13 this.moteur = new Moteur(type_moteur);
14
15 // agrégation => on recopie les passagers
16 this.passagers = new Passager[passagers.length];
17 for (int i = 0; i < passagers.length; i++) {
18 this.passagers[i] = passagers[i];
19 }
20 }
21
22 public static void main(String args[]) {
23 // les passagers existent avant que le SUV soit créé
24 Passager toto = new Passager("toto");
25 Passager titi = new Passager("titi");
26 Passager passagers[] = { toto, titi };
27
28 SUV suv = new SUV("V8", passagers);
29 }
30}
Quelle est la relation d’héritage/composition/agrégation entre les classes Cercle et Ellipse ?
Quelle est la relation entre les classes Personne et Employé ?
Quelle est la relation entre les classes Moto et Voiture ?
Quelle est la relation entre les classes Tableau et TableauRedimensionnable, où ces deux classes permettent de stocker des éléments, comme la classe MyArray de la séance 2 ?
Dans un jeu de rôle, on a des ennemis de type Orc, Elfe, Gobelin, Humain, Nain. On a défini une classe générique Ennemi ainsi que trois classes Force, Intelligence et Dexterite qui définissent les caractéristiques des ennemis. Quelles sont les relations entre ces différentes classes ?
Une classe Personne est en relation avec des classes Bras, Jambe, Vetement, Ordinateur. Déterminez quelles sont les relations (composition/agrégation) entre ces classes et Personne.
Une classe Voiture est en relation avec des classes Roue, Phare, Chassis, Bagages (les bagages des occupants du véhicule). Quelles sont les relations entre ces classes ?
Une classe HashTable représente une table de hachage dont les clefs et les valeurs sont des String. Quelle est la relation entre HashTable et String ?
Contrairement à C++ ou Python, en Java, on ne peut hériter que d’une seule classe. Autrement dit, l’héritage multiple est interdit. Le mot-clef pour indiquer que l’on hérite d’une autre classe est : extends. Sur la ligne 5 ci-dessous, on indique donc qu’un SUV hérite de la classe Vehicule. Les constructeurs de SUV doivent donc appeler ceux de Vehicule. C’est ce que fait le mot-clef super, comme en Python (cf. la ligne 10). Comme on a hérité, on peut, dans un SUV, appeler les méthodes de Vehicule (cf. les lignes 17, 18).
1package fr.polytech.aco.vehicule.voiture;
2
3import fr.polytech.aco.vehicule.Vehicule;
4
5public class SUV extends Vehicule {
6 private String nom;
7 private int cylindree;
8
9 public SUV(String nom, int cylindree) {
10 super(4); // le nombre de roues
11 this.cylindree = cylindree;
12 }
13
14 public static void main(String args[]) {
15 SUV suv = new SUV("V8", 120);
16 Vehicule suv2 = new SUV("V10", 160);
17 System.out.println(suv.getNbRoues());
18 System.out.println(suv2.getNbRoues());
19 }
20}
package fr.polytech.aco.vehicule;
public class Vehicule {
private int nbRoues;
protected Vehicule(int nbRoues) {
this.nbRoues = nbRoues;
}
public int getNbRoues() {
return this.nbRoues;
}
}
Si vous souhaitez savoir si un objet est une instance d’une classe particulière, il suffit d’utiliser l’instruction instanceOf :
if (suv instanceOf Vehicule) { .... }
Note
Par défaut, si vous n’utilisez pas le mot-clef extends, vos classes hériteront de la classe de « base » nommée Object. Vous verrez par la suite que cet héritage implicite sera bien utile.
Tout langage objet qui se respecte supporte la redéfinition de méthode, aussi appelée overriding ou polymorphisme de substitution, et la surcharge (ou overloading, ou polymorphisme ad-hoc):
Définition
L”overriding consiste à réécrire dans une classe enfant le code d’une méthode définie dans une classe parente, en conservant exactement les mêmes paramètres et le même type de retour.
En Java, il est utile d’annoter une telle méthode avec @Override. Ce n’est pas obligatoire mais c’est une bonne pratique car cela oblige le compilateur à effectuer quelques tests pour vérifier que vous êtes bien en train de redéfinir une méthode existante. Par ailleurs, cela rend le code plus lisible.
Définition
La surcharge consiste à écrire une méthode qui a le même nom qu’une méthode existante mais pas les mêmes paramètres ni, éventuellement, le même type de retour.
Ces deux mécanismes participent à ce que l’on appelle le polymorphisme :
Définition
Le polymorphisme, du grec poly (plusieurs) et morph (forme), est la capacité à fournir différents comportements au travers d’une même « interface ». Par exemple, le polymorphisme ad-hoc permet d’appeler deux méthodes ayant exactement le même nom et, pourtant, d’obtenir des résultats différents. Il existe un autre type de polymorphisme important en orienté objet : le polymorphisme paramétré, que l’on appelle également généricité et que nous verrons plus tard.
Par exemple, rajoutons à la classe SUV ci-dessus les deux méthodes suivantes:
@Override
public int getNbRoues() {
return 100;
}
public double getNbRoues(boolean inclut_roue_secours) {
if (inclut_roue_secours)
return (double) super.getNbRoues() + 1;
else
return (double) super.getNbRoues();
}
La première méthode redéfinit la méthode getNbRoues() de Vehicule sans modifier ses paramètres ni son type de retour. Il s’agit donc de polymorphisme de substitution. La deuxième méthode modifie les paramètres et le type de retour par rapport à ce qui est défini dans la classe Vehicule. Il s’agit donc de surcharge. Si le code du main() est le suivant :
public static void main(String args[]) {
SUV suv = new SUV("V8", 120);
Vehicule suv2 = new SUV("V10", 160);
System.out.println(suv.getNbRoues(true));
System.out.println(suv2.getNbRoues());
}
à l’exécution, on obtiendra :
5.0
100
Le compilateur ou la machine virtuelle Java détermira quelle est la bonne méthode à appliquer.
Si l’on crée maintenant une nouvelle classe Moto :
package fr.polytech.aco.vehicule.deuxRoues;
import fr.polytech.aco.vehicule.Vehicule;
public class Moto extends Vehicule {
private String nom;
public Moto(String nom) {
super(2);
this.nom = nom;
}
}
et que l’on crée une moto dans le main() :
public static void main(String args[]) {
SUV suv = new SUV("V8", 120);
Vehicule suv2 = new SUV("V10", 160);
Moto moto = new Moto("harley");
System.out.println(suv.getNbRoues(true));
System.out.println(suv2.getNbRoues());
System.out.println(moto.getNbRoues());
}
on obtiendra :
5.0
100
2
Autrement dit, les deux premières lignes proviennent des méthodes getNbRoues() de la classe SUV tandis que la troisième provient de celle de Vehicule.
On considère trois classes Forme, Cercle et Rectangle. La classe Forme possède un constructeur et deux méthodes :
un constructeur Forme(double x, double y) qui spécifie la localisation du centre de la forme dans le plan.
une méthode double aire()” qui renvoie l’aire (la surface) de la forme. La classe `Forme l’implémente en retournant toujours 0.
une méthode double distanceOrigine() qui indique la distance entre l’origine du plan (x=0, y=0) et le centre de la forme.
Évidemment, un Cercle et un Rectangle sont des Forme particuliers. Écrivez ces trois classes en Java.
On considère une classe qui permet de calculer la somme de 2, 3 ou 4 nombres réels. Écrivez une classe Calc qui permet de faire cela. Faites en sorte que, si on modifie l’opération arithmétique à effectuer, on ait à réécrire un minimum de code (pas plus d’une seule méthode). Ainsi, le main() suivant, où f est la méthode qui calcule la somme, devrait fonctionner :
public static void main(String[] args) {
var calc = new Calc();
System.out.println(calc.f(1.1, 2.2));
System.out.println(calc.f(1.1, 2.2, 3.3));
System.out.println(calc.f(1.1, 2.2, 3.3, 4.4));
}
On change maintenant la somme en la fonction suivante :
où X est un ensemble de nombres réels.
Mettez à jour votre classe Calc.