Composition, association, héritage

images/whitebelt.png

Définition d’un objet à partir d’autres objets: composition, association

Supposons que nous écrivions une gestion du personnel:

Définition d’un objet à partir d’autres objets. 

package com.maboite.management.rh ;

public class Employe {
   private String id;
   private int niveau ;
   private String nom ;
   private Adresse adresse ; // référence vers un autre type utilsateur
   private Manager manager ; // autre référence ...
   // .... ainsi de suite
   // Accesseurs
   public String getNom() { return this.nom ;}
   // ....
   // autres méthodes
   public double tauxHoraire() { /* code */ }
   public String toString() { /* code */ }
}

Ici le type Adresse a été créé dans une classe distincte plutot que d’avoir des champs d’adresse dans la classe Employe (séparation des fonctions, la classe Adresse est réutilisable dans d’autres contextes, …)

  • la référence adresse exprime une relation de composition.
  • la référence manager exprime une relation d’association .

Les instances correspondantes ne vont pas être gérées de la même manière durant le cycle de vie de l’objet.

Exemples:

  • Suppression d’un objet Employé : on peut supposer qu’on supprimera l’objet Adresse associé … mais pas le Manager!
  • Envoi d’un tel objet au travers du réseau: l’ordinateur cible va recomposer localement un objet Employe il aura sans doute besoin de adresse (dont les informations seront expédiées au travers du réseau); mais il n’aura peut-être pas besoin du manager (ci celui-ci existe déjà sur la machine cible: il faudra alors mettre au point des procédures particulières de reconstitution de l’instance).

Définition d’un objet à partir d’un autre objet: délégation

Maintenant une autre partie de notre application s’occupe de la gestion des tâches (pour gérer les taux d’occupation des employés).

délégation. 

package com.maboite.management.taches ;
import com.maboite.management.rh.Employe ;

public class Executant {
   private final Employe exécutant ;
   private Tache[] tâches ;
   // ... autres champs
   // méthodes spécifiques à la gestion des tâches
   public double revenu() { /* code */ }
   // ......

   public Executant(Employe employé) { this.exécutant = employé ; }

   // méthode définie par délégation
   public int getNiveau() { return exécutant.getNiveau() ; }
   public int getNom() { return exécutant.getNom() ; }
   public double tauxHoraire() { return exécutant.tauxHoraire() * MARGE_STANDARD ; }
}

Ici il y a une aggrégation forte entre l’instance d'Executant et l’instance d'Employe. La réalisation de certaines méthodes d'Executant s’appuie sur les services offert par la classe Employe (on évite ainsi des duplications de code).

Définition d’un objet à partir d’un autre objet: définition différentielle (héritage)

Essayons de définir une classe Manager :

 String id
 int niveau
 String nom
 Adresse adresse
 Manager manager
 ....
 Employe[] subordonnés
 ....
//  accesseurrs
 String getNom()
 Employe[] getSubordonnés()
...
// autres méthodes
 double tauxHoraire()
 String toString()
...

De nombreux codes ont déjà été réalisés pour la classe Employe et, d’ailleurs, un Manager EST UN Employe. Au lieu de dupliquer des codes on peut bâtir une définition différentielle dans laquelle le code de Manager ne contient que les différences explicites avec le code de Employe.

Une définition différentielle (simplifiée). 

package com.maboite.management.rh ;

public class Manager extends Employe {
   // uniquement les différences
   private Employe[] subordonnés
   public Employe[] getSubordonnés() {  return this.subordonnés ; }
   //..... autres  codes spécifiques au Manager

Ici le mot-clef extends est assez explicite: le code de Manager s’appuie sur celui de Employe … en rajoutant/modifiant certaines définitions.

Définition d’une sous-classe

Manager est une sous-classe d'Employe .

Employe est la super-classe de Manager.

héritage. 

package com.maboite.management.rh ;

public class Manager extends Employe {
   //  nouveaux champs
   private Employe[] subordonnés ;
   //....
   // nouvelles méthodes
   public Employe[] getSubordonnés() {
           return this.subordonnés.clone() ;
           // nous verrons que ce n'est pas la bonne idée
      // subordonnés sera une Liste
        }

   // méthodes "spécialisées"
   //  méthodes de la classe Employe dont la définition change
   public double tauxHoraire() {
      // code différent
   }
   public String toString() {  // code peu différent
      // s'appuie sur le toString() de la super-classe
      return super.toString()
         + "; nb subordonnés: "
            + this.subordonnés.length ;
   }

   // .. constructeurs
}

Regardons les différences et les partages avec le code de Employe:

  • Il y a des données spécifiques à la classe comme subordonnés et des méthodes associées.

- Il y a des codes qui sont des redéfinitions de méthodes de la super-classe: on dit que la méthode a été spécialisée (en Anglais l’opération se dit overriding retenez ce terme car il est souvent utilisé dans la documentation - par exemple avec des formes verbales comme"overrides")

+ Ici la méthode tauxHoraire a un code différent de celui de la super-classe.

  • Méthode spécialisée avec appel du code de la super-classe. C’est le cas de toString() on a une définition différentielle: le code de la méthode fait appel au code de toString() de la super-classe par l’expression super.toString()
  • Des méthodes comme getNom() sont héritées : on peut invoquer ces méthodes sur des instances de Manager (bien qu’elles ne soient pas définies dans cette classe).

    [Note]

    Quand on cherche une méthode dans la documentation d’une classe il faut chercher éventuellement dans la liste "Methods inherited from class XXXX".

--exercices--

Constructeurs dans une sous-classe

Les constructeurs ne sont pas "hérités" de la super-classe (les constructeurs ne sont pas des membres de la classe!): vous devez explicitement préciser les conditions de création des instances de la nouvelle classe.

Attention: un Manager aura un nom (membre "hérité") donc le(ou les) constructeur(s) de Manager a (ou auront) toutes les chances d’avoir un paramètre pour fixer ce nom. Or le code du constructeur de la sous-classe Manager n’a pas accès au champ privé nom de la super-classe Employe! On est donc dans une situation paradoxale: Manager a un nom mais ne peut pas y accéder (on n’a pas mis de méthode publique setNom dans la super-classe car on va considérer, peut être à tort, que c’est un champ immuable).

[Note]Rappel

Si vous vous demandez "pourquoi" Manager n’a pas accès aux champ "privés" de sa super-classe rappelez-vous que les deux codes peuvent relever de responsabilités différentes. Ce ne sont pas forcément les mêmes programmeurs qui ont écrit ces deux codes et, de plus, ce peut être des programmeurs appartenant à des équipes différentes et réalisant des packages différents (la sous-classe et sa super-clasee peuvent être dans des packages différents. En fait c’est très courant, par exemple quand on spécialise des classes standard de java).

Comment permettre au constructeur de Manager de réaliser les initialisations liées à son héritage?

Si la classe Employe a un constructeur tel que:

public Employe(String nom, String id, int niveau, Adresse adresse)

constructeur dans une sous-classe. 

package com.maboite.management.rh ;

public class Manager extends Employe {
   //  autres éléments du code
   public Manager(String nom, String id, int niveau, Adresse adresse,
            Employe[] subordonnés) {
      super(nom, id, niveau, adresse) ; // zone 1 du constructeur
                                    // zone 2 (implicite)
      this.subordonnés = subordonnés ;// zone 3
      // ainsi de suite ...
   }

}

Ici la première instruction du code du constructeur va être une invocation du code du constructeur de la super-classe. Ceci se fait par l’invocation de super(..paramètres).

[Attention]Attention!

(points complexes à étudier ultérieurement)

  • Le bloc associé à un constructeur commence par une appel à this(°°°°) ou un appel à super(°°°°). (zone 1).
  • S’il n’y a aucun de ces appels le compilateur tente d’introduire un appel à super() (sans argument):

    • si la super-classe n’a pas de constructeur sans paramètre la compilation va échouer!
    • si on ne définit pas de constructeur dans cette sous-classe le compilateur va tenter de générer un constructeur par défaut (sans argument) et celui-ci fera un appel à super() (et la compilation échouera si un constructeur sans argument n’existe pas dans la super-classe!).
  • pour chaque invocation d’un constructeur il y a (récursivement) :

    • un appel au super-constructeur (zone 1 ) -dans certains cas il y a d’abord appel à un autre constructeur de la classe courante via this(°°°)-
    • un appel aux initialisations des membres déclarés dans la classe (zone 2)
    • execution des instructions du bloc (zone 3)
[Note]

Il n’y a pas d'héritage multiple en Java (une classe ne peut pas faire extends de plusieurs classes).

Pourquoi cette option alors que d’autres langages de programmation autorisent l’héritage multiple? Encore une fois il s’agit d’une précaution pour rendre plus explicite le comportement des codes. L’héritage multiple peut poser problème en cas d’arbre d’héritage complexe : il peut devenir difficile d’appréhender quel est le comportement attendu de l’objet. (il y a déjà quelques problèmes de ce type avec l’héritage d’interfaces dont nous parlerons plus tard).

--suite des exercices--

Polymorphisme

Dans la mesure où un Manager est un Employé on peut écrire:

Employe emp = new Manager( nom, niveau, adresse, subs) ;

L’objet référencé est de type effectif Manager mais la référénce ne connait que les caractéristiques d'Employe. Les références sont polymorphes : elle peuvent désigner des objets de types différents mais ne connaissent que les caractéristiques liées au type déclaré.

   Employe emp = new Manager( nom, niveau, adresse, subs) ; // référence polymorphe
   System.out.println(emp.getNom()) ; // ok
   System.out.println(emp.getSubordonnés().length) ; /* _compiler_error_ */

Conséquences:

   Employe[] équipe = {
       new Employe( nom1, niveau1, adresse1) ,
       new Employe( nom2, niveau2, adresse2) ,
       new Manager( nom, niveau, adresse, subs) ,
   } ;

   for (Employe emp : équipe ) {
      System.out.println(emp.toString()) ;
   }

Le tableau est légal: équipe est un tableau d'Employe et un Manager est un Employe!

Comme la méthode toString() est réalisée de manière différente dans les classes Employe et Manager …. quelle est la méthode qui va être appelée pour l'élément équipe[2] ?

C’est la méthode définie pour la classe Manager!

On invoque une méthode sur une référence polymorphe et c’est la méthode du type effectif qui est exécutée ("virtual method invocation").

Ceci est une caractéristique essentielle de Java. Si on écrit, par exemple, un code RH qui calcule la paye à partir de tauxHoraire() ce code sera écrit une bonne fois pour toutes. Si on crée ultérieurement une classe PDG pour laquelle le tauxHoraire est calculé différemment on n’a pas à modifier le code précédent! (Il est notoire qu'écrire un test du genre si c’est un Manager faire ceci; si c’est un PDG faire cela; … relève d’une mauvaise programmation).

--exercices-- et --autres exercices--

La classe Object

images/orangebelt.png

Il y a une classe standard qui est la super-classe de toutes les classes java: la classe Object .

Vous pouvez constater que chaque fois que vous lisez la documentation d’une classe quelconque la class Object se trouve au sommet de la hiérarchie d’héritage! Même si vous définissez une classe sans extends elle hérite en fait de Object.

Extrait du pseudo-code de la classe Calimero:

Compiled from "Calimero.java"
public class Calimero extends java.lang.Object{
public Calimero();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

On peut utiliser le type Object chaque fois que l’on veut écrire un code très général pour stocker des instances de n’importe quoi:

Object[] tableauDeNImporteQuoi ;
[Note]

Dans un tel tableau on peut même stocker des scalaires "emballés" automatiquement par le compilateur ( autoboxing/unboxing ).

spécialisation des méthodes de Object

La classe Object a ses propres méthodes. On est donc déjà sûr de pouvoir appeler ces méthodes sur n’importe quel objet:

   public void doIt(Object arg) { //
      Class vraieClasse = arg.getClass() ;
      // utilisé  rarement: recherche dynamique de type
      // package java.lang.reflect
      //.....
      System.out.println(arg.toString()) ;
      System.out.println(vraieClasse.toString()) ;

Plusieurs des méthodes de Object sont fréquement spécialisées lorsqu’on définit une classe.

Spécialisation de toString()

package com.bankcalor.finance;
import java.time.Instant ;

public class Versement {
   final double montant ;
   final String monnaie ;
   final long heureOperation = System.currentTimeMillis();

   public Versement(double val, String monn) {
        this.montant = val ;
        this.monnaie = monn ;
   }

   // spécialisation de la méthode de Object
   public String toString() {
       return this.montant + " " + this.monnaie
               + " (" + Instant.ofEpochMilli(this.heureOperation )+ ")";
   }

}

Comme chaque objet a une méthode toString() la méthode println(Object arg) de java.io.PrintStream appelle en fait la méthode toString() de son argument. On peut alors écrire:

   Versement versement ;
   //...
   System.out.println(versement) ;

Spécialisation de equals

On ne peut pas utiliser l’opérateur == pour comparer deux objets: on doit utiliser la méthode equals(Object autre) de Object. Mais comme cette méthode agit par défaut en comparant les références elle est en général redéfinie dans le code des classes. lire attentivement la documentation du "contrat de service" de la méthode equals.

Par exemple ce code est erroné:

public class Employe {
   private String id ;
   // codes .....
   public boolean equals(Employe autre) {
      return this.id.equals(autre.id) ;
   }
}

Dans l’exemple ci-dessus la méthode surcharge (en Anglais overloading) la méthode equals mais ne la spécialise pas en Anglais overriding-! La plupart des codes standard risquent de ne pas appeler la bonne méthode!

Spécialisation de equals

public class Employe {
   private String id ;
   // codes .....
   @Override 1
   public boolean equals(Object autre) {
      return ( autre instanceof2 Employe)
       && ( this.id.equals( ((Employe)3 autre).id )) ;
   }
}

1

Cette annotation indique au compilateur votre intention de spécialiser la méthode correspondante de la super-classe. Si ce n’est pas le cas le compilateur vous préviendra!

2

L’opérateur instanceof est utilisé pour tester si une référence est conforme à un contrat de type ( en l’occurence: la référence est un Employe ou d’un type dérivé).

3

transtypage (cast) sur une référence. Ce n’est pas une conversion(comme dans (int) valeurLong) l’objet cible n’est pas modifié. Au compile time ceci va permettre au compilateur d’accepter l’utilisation du champ id d'Employe. Au runtime cela permet de contrôler si l’instance autre est conforme au contrat de type Employe (ce qui ne se produira pas ici puisque le test instanceof filtre déjà). En cas d'échec une ClassCastException est déclenchée.

Spécialisation de equals (autre exemple). 

public class Paire {
   private double valA ;
   private double valB ;

   // ....
   @Override
   public boolean equals(Object autre) {
      return (autre instanceof Paire) && this.equals((Paire) autre) ;
   }

   // surcharge de equals
   public boolean equals(Paire autrePaire) {
      return (this.valA == autrePaire.valA) && (this.valB == autrePaire.valB) ;
   }
}

[Avertissement]

Les outils intégrés (I.D.E) proposent de générer "automatiquement" les méthodes equals et la méthode hashCode qui lui est liée. Faire extrèmement attention à ces générations car les codes par défaut peuvent avoir des propriétés non voulues -ce point sera discuté ultérieuremnt-

--exercice--

Modules, responsabilité et relations entre classes

Voir les exercices --ICI--

Modificateur de portée protected

images/orangebelt.png

La classe Manager est une sous-classe d'Employe: une instance de Manager a donc un champ niveau …. mais ne peut pas y accéder directement! Ce champ est sous la responsabilité du programmeur de la classe Employe. Ceci dit un programmeur pourrait "entrouvrir" une partie de son code à un accès privilégié depuis les instances des sous-classes.

package com.maboite.management.taches ;

public class GestionnaireTaches {
   private Tache[] taches = new Tache[TAILLE_STD];
   private int nbTaches ;
   //.....
   public void ajout(Tache tacheCourante) {
      // on fait qqch ...
      // puis
      taches[nbTaches++] = tacheCourante ;
      // autres codes
   }
}

Si le programmeur responsable veut permettre aux codes des sous-classes de spécialiser ajout(Tache) il peut "exposer" le tableau taches (de manière à ce que ces codes accèdent à ce tableau).

Ce n’est pas forcément une bonne idée si le code de la classe de référence peut changer ultérieurement (par exemple remplacement du tableau par une classe Collection). On pourrait donc "entrouvrir" le code différemment:

Code définissant un accès protected

package com.maboite.management.taches ;

public class GestionnaireTaches {
   private Tache[] taches = new Tache[TAILLE_STD];
   private int nbTaches ;
   //.....
   protected void enregistrer(Tache tache) {
      //la réalisation de ce code peut changer
      taches[nbTaches++] = tache ;
   }
   public void ajout(Tache tacheCourante) {
      // on fait qqch ...
      // puis
      this.enregistrer(tacheCourante) ;
      // autres codes
   }
}

Code utilisant un accès protected

package  org.boulot.evals ; //  nous sommes dans un package différent

import com.maboite.management.taches.* ;

public class NotrePlanning extends GestionnaireTaches {
   @Override
   public void ajout(Tache tacheCourante) {
      // on fait qqch ... mais différemment
      // puis
      this.enregistrer(tacheCourante) ;
      // autres codes
   }
}

[Attention]Attention!

L’accès à un membre d’instance marqué protected se fait uniquement au travers d’une référence du type de la classe courante (en général au travers de this) -on ne peut utiliser aucun autre type y compris le type de la super-classe!-

Il est faux de dire que ce qui est protected est accessible par les codes des sous-classes. Ce code ne se compilera pas:

package  org.boulot.evals ; //  nous sommes dans un package différent

import com.maboite.management.taches.* ;

public class UtilPlanning extends GestionnaireTaches {
     public void modification(GestionnaireTaches gestionnaire, Tache tache) {
       gestionnaire.enregistrer(tache) ; // _compiler_error_ !!!!
     }

protected trouble beaucoup de programmeurs! Pour fixer un peu les idées on peut dire qu’un membre (ou un constructeur) protected est avant tout un mécanisme interne de l’instance mis à la disposition des sous-classes il ne participe souvent pas aux services "métier" de la classe. Etant un mécanisme de réalisation les éléments protected sont accessibles par les autres équipiers qui participent à la réalisation du même package (ils sont accessibles par les codes du même package).

Les membres et constructeurs protected font partie de l’A.P.I. : on ne peut plus changer leur signature une fois que la classe est publiée.

Il y a d’autres utilisations de protected (un exemple complexe ici: la méthode clone()). Parfois on a également une méthode protected qui est conçue pour être spécialisée et rendue public.

final et l’héritage

images/orangebelt.png

On ne peut pas écrire un code qui hériterait d’une classe marquée final. C’est souvent le cas avec des classes qui décrivent des objets "valeurs" ( value objects comme String, Integer, Double, …) -mais, par exemple, les objets java.math.BigDecimal ne sont pas final-

Une méthode marquée final ne peut pas être spécialisée.

Lorqu’une Applet crée une fenêtre indépendante de celle du navigateur il apparaît au bas de la fenêtre un bandeau avec un message : " unsigned Applet Window". Ce message provient de l’exécution de cette méthode (getWarningString). Comme n’importe quel programmeur peut écrire un code qui hérite de java.awt.Window (ou, plus probablement, de java.awt.Frame qui hérite de Window), à votre avis, pourquoi cette méthode est-elle final ?