Héritage et polymorphisme

Chapitres traités   

 

 

 

 

Choix des chapitres La super-classe Object

Jusqu'ici, nous pouvons considérer que nous avons défini deux sortes de classes : des classes simples et des classes dérivées. En réalité, il existe une classe nommée Object dont dérive implicitement toute classe simple. Ainsi, lorsque vous définissez une classe Point de cette manière :

class Point {
...
}

tout se passe en fait comme si vous aviez écrit (vous pouvez d'ailleurs le faire) :

class Point extends Object {
...
}

Voyons les conséquences de cette propriété.

Utilisation d'une référence de type Object

Compte tenu des possibilités de compatibilité exposées précédemment, une variable de type Object peut être utilisée pour référencer un objet de type quelconque :

Point p = new Point (...) ;
Pointcol pc = new Pointcol (...) ;
Fleur f = new Fleur (...) ;
Object o ; o = p ; OK
o = pc ; OK
o = f ; OK

Cette particularité peut être utilisée pour manipuler des objets dont on ne connaît pas le type exact (au moins à un certain moment). Cela pourrait être le cas d'une méthode qui se contente de transmettre à une autre méthode une référence qu'elle a reçu en argument.

Bien entendu, dès qu'on souhaitera appliquer une méthode précise à un objet référencé par une variable de type Object, il faudra obligatoirement effectuer une conversion appropriée. Voyez cet exemple où l'on suppose que la classe Point dispose de la méthode déplace

Point p = new Point (...) ;
Object o ;
...
o = p ;
o.déplace() ; erreur de compilation
((Point)o).déplace() ; OK en compilation (attention aux parenthèses)
Point p1 = (Point) o ; OK : idem ci-dessus, avec création d'une référence
p1.déplace() ; intermédiaire dans p1

Notez bien les conséquences des règles relatives au polymorphisme. Pour pouvoir appeler une méthode f par o.f(), il ne suffit pas que l'objet effectivement référencé par f soit d'un type comportant une méthode f, il faut aussi que ladite méthode existe déjà dans la classe Object. Ce n'est manifestement pas le cas de la méthode déplace.

Choix du chapitre Utilisation de la méthode toString() de la classe Object

Une méthode importante de la classe Object est la méthode toString(), qui renvoie une chaîne de caractères représentant la valeur de l'objet. Cette méthode est intéressante puisque lorsque vous demander d'afficher l'objet en entier, c'est cette méthode toString() qui est invoquée. L'intérêt, c'est que sans écriture particulière, vous obtenez malgré tout un affichage par défaut.

Méthode toString() redéfinies dans les classes de la JDK

import java.awt.Point;

public class Calcul {
   public static void main(String[] args) {
      System.out.println(new Point(15, 23));
   }
}

Ainsi, la méthode toString() de la classe Point renvoie une chaîne comme celle-ci :

java.awt.Point[x=15,y=23]

Vous remarquez, que les classes de la JDK redéfinisse la méthode toString() de la classe de base Object. La plupart (mais pas toutes) des méthodes toString() redéfinies dans les différents objets propose un format similaire : le nom de la classe, suivi des valeurs des attributs incluts entre crochets.

Classes personnalisées

Que se passe-t-il si lorsque nous créons nos propres classes et que nous tentons de les afficher directement, comme au travers de l'exemple ci-dessous ?

import java.awt.Point;

public class Calcul {
   public static void main(String[] args) {
      System.out.println(new Personne("REMY", "Emmanuel", 46));
   }
}

class Personne {
   private String nom, prénom;
   private int âge;
   public Personne(String nom, String prénom, int âge) {
      this.nom = nom;
      this.prénom = prénom;
      this.âge = âge;
   }
}

L'affichage est alors le suivant :

Personne@82ba41

En fait, la classe Object définit la méthode toString() pour afficher le nom de la classe et le code de hachage de l'objet (ce code est un numéro de référence qui représente l'objet au sein de la JDK). Comme notre classe ne redéfinit par cette méthode toString(), c'est celle de la classe Object (par héritage : toute classe hérite au moins de la classe de base Object) qui est sollicité.

Redéfinition de la méthode toString() pour nos classes personnalisées

Ainsi, l'affichage de base de la classe Personne n'est pas très agréable. Il est préférable de proposer sa propre version et donc de redéfinir la méthode toString(). En voici une solution possible :

import java.awt.Point;

public class Calcul {
   public static void main(String[] args) {
      System.out.println(new Personne("REMY", "Emmanuel"));
   }
}

class Personne {
   private String nom, prénom;
   public Personne(String nom, String prénom) {
      this.nom = nom;
      this.prénom = prénom;
   }
   public String toString() {
      return "Personne[nom="+nom+", prénom="+prénom+"]";
   }
}

Voici le résultat obtenu :

Elève[nom=REMY, prénom=Emmanuel][notes = 15.0 8.0 12.5 ]

En réalité, vous pouvez faire mieux. Au lieu de coder en dur le nom de la classe dans la méthode toString(), appelez getClass().getName() pour obtenir une chaîne avec le nom de la classe. Ainsi, la méthode toString() s'applique également aux sous-classes.

Bien entendu, le programmeur de la sous-classe doit définir sa propre méthode toString() et ajouter les attributs de la sous-classe. Si la superclasse utilise getClass().getName(), la sous-classe peut simplement appeler super.toString().

import java.awt.Point;
import java.util.ArrayList;

public class Calcul {
   public static void main(String[] args) {
      Elève élève = new Elève("REMY", "Emmanuel");
      élève.ajout(15.0).ajout(8.0).ajout(12.5); 
      System.out.println(élève);
   }
}

class Personne {
   private String nom, prénom;
   public Personne(String nom, String prénom) {
      this.nom = nom;
      this.prénom = prénom;
   }
   public String toString() {
      return getClass().getName()+"[nom="+nom+", prénom="+prénom+"]";
   }
}

class Elève extends Personne {
   private ArrayList<Double> notes = new ArrayList<Double>();
   public Elève(String nom, String prénom) { super(nom, prénom); }
   public Elève ajout(double note) { notes.add(note); return this; }
   public String toString() {
      String chaîne = super.toString()+"[notes = ";
      if (notes.isEmpty()) chaîne+="aucune ]";
      else for (Double x : notes) chaîne = chaîne + x +" ";
      return chaîne+="]";
   }
}

La classe Elève récupère le comportement de l'affichage par défaut de sa classe parente Personne. Cette dernière toutefois préfère donner le nom de la classe de façon polymorphe, donc soit d'elle même, soit d'une de ses sous-classe.

Ainsi, voici le résultat obtenu :

Elève[nom=REMY, prénom=Emmanuel][notes = 15.0 8.0 12.5 ]

La méthode toString() est omniprésente pour une raison essentielle : chaque fois qu'un objet est concaténé avec une chaine à l'aide de l'opérateur "+", le compilateur Java invoque automatiquement la méthode toString() pour obtenir une représentation de l'objet sous forme de chaîne de caractères.

import java.awt.Point;
import java.util.ArrayList;

public class Calcul {
   public static void main(String[] args) {
      Personne moi = new Personne("REMY", "Emmanuel");
      System.out.println("Mon nom est : "+moi);
   }
}

class Personne {
   private String nom, prénom;
   public Personne(String nom, String prénom) {
      this.nom = nom;
      this.prénom = prénom;
   }
   public String toString() {
      return getClass().getName()+"[nom="+nom+", prénom="+prénom+"]";
   }
}

Au lieu d'écrire x.toString(), vous pouvez écrire ""+x. Cette instruction concatène la chaîne vide avec la représentation textuelle de x, qui correspond exactement à la représentation obtenue par x.toString(). A la différence de toString(), cette instruction fonctionne, même si x est de type primitif.

Si x est un objet quelconque et que vous appeliez
System.out.println(x) ;
La méthode println() appelle simplement x.toString() et affiche la chaîne de carcatères résultante.

La méthode toString() est un outil précieux de consignation. de nombreuses classes de la bibliothèque de classes standard définissent la méthode toString() comme fournisseur d'informations utiles sur l'état d'un objet. Il est fortement recommandé d'ajouter une méthode toString() à chacune des classes que vous écrivez. Vous et tous les programmeurs utilisant vos classes apprécierez le support de consignation.

 

Choix des chapitres Classes et méthodes finales

Nous avons déjà vu comment le mot clé final pouvait s'appliquer à des variables locales ou à des champs de classe. Il interdit la modification de leur valeur. Ce mot clé peut aussi s'appliquer à une méthode ou à une classe, mais avec une signification totalement différente :

Une méthode déclarée final ne peut pas être redéfinie dans une classe dérivée.
.

Le comportement d'une méthode finale est donc complètement défini et il ne peut plus être remis en cause, sauf si la méthode appelle elle-même une méthode qui n'est pas déclarée final. L'écriture est la suivante :

public final void méthode() {
...
}

Une classe déclarée final ne peut plus être dérivée.
.

On est ainsi certain que le contrat de la classe sera respecté. L'écriture est la suivante :

public final class ExempleClasse {
...
}

On pourrait croire qu'une classe finale est équivalente à une classe non finale dont toutes les méthodes seraient finales. En fait, ce n'est pas vrai car :

  1. ne pouvant plus être dérivée, une classe finale ne pourra pas se voir ajouter de nouvelles fonctionnalités,
  2. une classe non finale dont toutes les méthodes sont finales pourra toujours être dérivée, donc se voir ajouter de nouvelles fonctionnalités.

Par son caractère parfaitement défini, une méthode finale permet au compilateur

  1. de détecter des anomalies qui, sans cela, n'apparaîtraient que lors de l'exécution,
  2. d'optimiser certaines parties de code : appels plus rapides puisque indépendants de l'exécution, mise "en ligne" du code de certaines méthodes...

En revanche, il va de soi que le choix d'une méthode ou d'une classe finale est très contraignant et ne doit être fait qu'en toute connaissance de cause. Beaucoup de classes intégrées dans le JDK sont finales, c'est le cas notamment de la classe String de la classe Math, etc...

 

Choix des chapitres Les classes abstraites

Une classe abstraite est une classe qui ne permet pas d'instancier des objets. Elle ne peut servir que de classe de base pour une dérivation. Elle se déclare ainsi

abstract class A {
...
}

Dans une classe abstraite, on peut trouver classiquement des méthodes et des champs, dont héritera toute classe dérivée. Mais on peut aussi trouver des méthodes dites abstraites, c'est-à dire dont on ne fournit que la signature et le type de la valeur de retour. Par exemple :

Bien entendu, on pourra déclarer une variable de type A :

A a ; // OK : a n'est qu'une référence sur un objet de type A ou dérivé

En revanche, toute instanciation d'un objet de type A sera rejetée par le compilateur :

a = new A(...) ; // erreur : pas d'instanciation d'objets d'une classe abstraite

En revanche, si on dérive de A une classe B qui définit la méthode abstraite g :

on pourra alors instancier un objet de type B par new B(..) et même affecter sa référence à une variable de type A :

A a = new B(...) ; // OK

Choix du chapitre Quelques règles

  1. Dès qu'une classe abstraite comporte une ou plusieurs méthodes abstraites, elle est abstraite, et ce même si l'on n'indique pas le mot clé abstract devant sa déclaration (ce qui reste quand même vivement conseillé). Ceci est correct :

    Malgré tout, A est considérée comme abstraite et une expression telle que new A(..) sera rejetée.

  2. Une méthode abstraite doit obligatoirement être déclarée public, ce qui est logique puisque sa vocation est d'être redéfinie dans une classe dérivée.
  3. Dans l'en-tête d'une méthode déclarée abstraite, les noms d'arguments muets doivent figurer (bien qu'ils ne servent à rien) :

  4. Une classe dérivée d'une classe abstraite n'est pas obligée de (re)définir toutes les méthodes abstraites de sa classe de base (elle peut même n'en redéfinir aucune). Dans ce cas, elle reste simplement abstraite (bien que ce ne soit pas obligatoire, il est conseillé de mentionner abstract dans sa déclaration) :

    Ici, B définit f1, mais pas f2. La classe B reste abstraite (même si on ne l'a pas déclarée ainsi).

  5. Une classe dérivée d'une classe non abstraite peut être déclarée abstraite et/ou contenir des méthodes abstraites.

Choix du chapitre Intérêt des classes abstraites

Le recours aux classes abstraites facilite largement la Conception Orientée Objet. En effet, on peut placer dans une classe abstraite toutes les fonctionnalités dont on souhaite disposer pour toutes ses descendantes :

  1. soit sous forme d'une implémentation complète de méthodes (non abstraites) et de champs (privés ou non) lorsqu'ils sont communs à toutes ses descendantes,
  2. soit sous forme d'interface de méthodes abstraites dont on est alors sûr qu'elles existeront dans toute classe dérivée instanciable.

C'est cette certitude de la présence de certaines méthodes qui permet d'exploiter le polymorphisme, et ce dès la conception de la classe abstraite, alors même qu'aucune classe dérivée n'a peut-être encore été créée. Notamment, on peut très bien écrire des canevas recourant à des méthodes abstraites. Par exemple, si vous avez défini :

vous pourrez écrire une méthode (d'une classe quelconque) telle que :

Bien entendu, la redéfinition de f devra, comme d'habitude, respecter la sémantique prévue dans le contrat de X.

Choix du chapitre Exemple

Voici un exemple de programme illustrant l'emploi d'une classe abstraite nommée Affichable, dotée d'une seule méthode abstraite affiche(). Deux classes Entier et Flottant dérivent de cette classe. La méthode main() utilise un tableau hétérogène d'objets de type Affichable qu'elle remplit en instanciant des objets de type Entier et Flottant.

Résultat :

Je suis un entier de valeur 25
Je suis un flottant de valeur 1.25
Je suis un entier de valeur 42

Pour connaître les classes internes