Héritage vs Composition / Agrégation

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 :

Héritage ou pas ?

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.

Exemple de composition vs agrégation

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.

SUV.java
 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}

Exercice 1 : Héritage, composition ou agrégation, ou ni l'un ni l'autre ?   

  1. Quelle est la relation d’héritage/composition/agrégation entre les classes Cercle et Ellipse ?

  2. Quelle est la relation entre les classes Personne et Employé ?

  3. Quelle est la relation entre les classes Moto et Voiture ?

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

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

Exercice 2 : Composition ou agrégation ?   

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 ?

L'héritage en Java

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

SUV.java
 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}
Vehicule.java
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.

Redéfinition, surcharge, polymorphisme

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 :

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

Exercice 3 : C'est la grande forme   

On considère trois classes Forme, Cercle et Rectangle. La classe Forme possède un constructeur et deux méthodes :

  1. un constructeur Forme(double x, double y) qui spécifie la localisation du centre de la forme dans le plan.

  2. une méthode double aire()” qui renvoie l’aire (la surface) de la forme. La classe `Forme l’implémente en retournant toujours 0.

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

Exercice 4 : En somme, c'est une formule compliquée   

  1. 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));
    }
    

    Indice 1 

    Pensez à utiliser la surcharge.

  2. On change maintenant la somme en la fonction suivante :

    \[f(X) = \sum_{x \in X} \left(\sqrt{x} + x \times \log(x) \right)^2\]

    où X est un ensemble de nombres réels.

    Mettez à jour votre classe Calc.

 
© C.G. 2007 - 2024