IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java

  Chapitre 7 - Polymorphisme

pages : 1 2 3 4 

Dans le main(), quand on met quelque chose dans le tableau d' Instrument, on upcast automatiquement en Instrument.

Vous pouvez constater que la méthode tune() ignore fort heureusement tous les changements qui sont intervenus autour d'elle, et pourtant cela marche correctement. C'est exactement ce que le polymorphisme est censé fournir. Vos modifications ne peuvent abîmer les parties du programme qui ne devraient pas être affectées. Dit autrement, le polymorphisme est une des techniques majeures permettant au programmeur de « séparer les choses qui changent des choses qui restent les mêmes. »

Redéfinition et Surcharge

Regardons sous un angle différent le premier exemple de ce chapitre. Dans le programme suivant, l'interface de la méthode play() est changée dans le but de la redéfinir, ce qui signifie que vous n'avez pas redéfinie la méthode, mais plutôt surchargée. Le compilateur vous permet de surcharger des méthodes, il ne proteste donc pas. Mais le comportement n'est probablement pas celui que vous vouliez. Voici l'exemple :


//: c07:WindError.java
// Changement accidentel de l'interface.
class NoteX {
  public static final int
    MIDDLE_C = 0, C_SHARP = 1, C_FLAT = 2;
}

class InstrumentX {
  public void play(int NoteX) {
    System.out.println("InstrumentX.play()");
  }
}

class WindX extends InstrumentX {
  // OUPS! L'interface de la méthode change:
  public void play(NoteX n) {
    System.out.println("WindX.play(NoteX n)");
  }
}

public class WindError {
  public static void tune(InstrumentX i) {
    // ...
    i.play(NoteX.MIDDLE_C);
  }
  public static void main(String[] args) {
    WindX flute = new WindX();
    tune(flute); // Ce n'est pas le comportement souhaité!
  }
} ///:~

Il y a un autre aspect déroutant ici. Dans InstrumentX, la méthode play() a pour argument un int identifié par NoteX. Bien que NoteX soit un nom de classe, il peut également être utilisé comme identificateur sans erreur. Mais dans WindX, play() prend une référence de NoteX qui a pour identificateur n (bien que vous puissiez même écrire play(NoteX NoteX) sans erreur). En fait, il s'avère que le programmeur a désiré redéfinir play() mais s'est trompé de type. Du coup le compilateur a supposé qu'une surcharge était souhaitée et non pas une redéfinition. Remarquez que si vous respectez la convention standard de nommage Java, l'identificateur d'argument serait noteX ('n' minuscule), ce qui le distinguerait du nom de la classe.

Dans tune, le message play() est envoyé à l'InstrumentX i, avec comme argument un de membres de NoteX (MIDDLE_C). Puisque NoteX contient des définitions d'int, ceci signifie que c'est la version avec int de la méthode play(), dorénavant surchargée, qui est appelée. Comme elle n'a pas été redéfinie, c'est donc la méthode de la classe de base qui est utilisée.

L'output est le suivant :


InstrumentX.play()

Ceci n'est pas un appel polymorphe de méthode. Dès que vous comprenez ce qui se passe, vous pouvez corriger le problème assez facilement, mais imaginez la difficulté pour trouver l'anomalie si elle est enterrée dans un gros programme.

Classes et méthodes abstraites

Dans tous ces exemples sur l'instrument de musique, les méthodes de la classe de base Instrument étaient toujours factices. Si jamais ces méthodes sont appelées, c'est que vous avez fait quelque chose de travers. C'est parce que le rôle de la classe Instrument est de créer une interface commune pour toutes les classes dérivées d'elle.

La seule raison d'avoir cette interface commune est qu'elle peut être exprimée différemment pour chaque sous-type différent. Elle établit une forme de base, ainsi vous pouvez dire ce qui est commun avec toutes les classes dérivées. Une autre manière d'exprimer cette factorisation du code est d'appeler Instrument une classe de base abstraite (ou simplement une classe abstraite). Vous créez une classe abstraite quand vous voulez manipuler un ensemble de classes à travers cette interface commune. Toutes les méthodes des classes dérivées qui correspondent à la signature de la déclaration de classe de base seront appelées employant le mécanisme de liaison dynamique. (Cependant, comme on l'a vu dans la dernière section, si le nom de la méthode est le même comme la classe de base mais les arguments sont différents, vous avez une surcharge, ce qui n'est pas probablement que vous voulez.)

Si vous avez une classe abstraite comme Instrument, les objets de cette classe n'ont pratiquement aucune signification. Le rôle d'Instrument est uniquement d'exprimer une interface et non pas une implémentation particulière, ainsi la création d'un objet Instrument n'a pas de sens et vous voudrez probablement dissuader l'utilisateur de le faire. Une implémentation possible est d'afficher un message d'erreur dans toutes les méthodes d'Instrument, mais cela retarde le diagnostic à l'exécution et exige un code fiable et exhaustif. Il est toujours préférable de traiter les problèmes au moment de la compilation.

Java fournit un mécanisme qui implémente cette fonctionnalité: c'est la méthode abstraite [37]. C'est une méthode qui est incomplète; elle a seulement une déclaration et aucun corps de méthode. Voici la syntaxe pour une déclaration de méthode abstraite [abstract] :


abstract void f();

Une classe contenant des méthodes abstraites est appelée une classe abstraite. Si une classe contient une ou plusieurs méthodes abstraites, la classe doit être qualifiée comme abstract. (Autrement, le compilateur signale une erreur.)

Si une classe abstraite est incomplète, comment doit réagir le compilateur si quelqu'un essaye de créer un objet de cette classe? Il ne peut pas créer sans risque un objet d'une classe abstraite, donc vous obtenez un message d'erreur du compilateur. C'est ainsi que le compilateur assure la pureté de la classe abstraite et ainsi vous n'avez plus a vous soucier d'un usage impropre de la classe.

Si vous héritez d'une classe abstraite et que vous voulez fabriquer des objets du nouveau type, vous devez fournir des définitions de méthode correspondant à toutes les méthodes abstraites de la classe de base. Si vous ne le faites pas (cela peut être votre choix ), alors la classe dérivée est aussi abstraite et le compilateur vous forcera à qualifier cette classe avec le mot clé abstract.

Il est possible de créer une classe abstraite sans qu'elle contienne des méthodes abstraites. C'est utile quand vous avez une classe pour laquelle avoir des méthodes abstraites n'a pas de sens et que vous voulez empêcher la création d'instance de cette classe.

La classe Instrument peut facilement être changée en une classe abstraite. Seules certaines des méthodes seront abstraites, puisque créer une classe abstraite ne vous oblige pas a avoir que des méthodes abstraites. Voici à quoi cela ressemble :

Image

Voici l'exemple de l'orchestre modifié en utilisant des classes et des méthodes abstraites :


//: c07:music4:Music4.java
// Classes et méthodes abstraites.
import java.util.*;

abstract class Instrument {
  int i; // Alloué à chaque fois
  public abstract void play();
  public String what() {
    return "Instrument"size="4">;
  }
  public abstract void adjust();
}

class Wind extends Instrument {
  public void play() {
    System.out.println("Wind.play()");
  }
  public String what() { return "Wind"; }
  public void adjust() {}
}

class Percussion extends Instrument {
  public void play() {
    System.out.println("Percussion.play()");
  }
  public String what() { return "Percussion"; }
  public void adjust() {}
}

class Stringed extends Instrument {
  public void play() {
    System.out.println("Stringed.play()");
  }
  public String what() { return "Stringed"; }
  public void adjust() {}
}

class Brass extends Wind {
  public void play() {
    System.out.println("Brass.play()");
  }
  public void adjust() {
    System.out.println("Brass.adjust()");
  }
}

class Woodwind extends Wind {
  public void play() {
    System.out.println("Woodwind.play()");
  }
  public String what() { return "Woodwind"; }
}

public class Music4 {
  // Ne se préoccupe pas des types: des nouveaux
  // ajoutés au système marcheront très bien:
  static void tune(Instrument i) {
    // ...
    i.play();
  }
  static void tuneAll(Instrument[] e) {
    forint i = 0; i < e.length; i++)
      tune(e[i]);
  }
  public static void main(String[] args) {
    Instrument[] orchestra = new Instrument[5];
    int i = 0;
    // Upcast lors de l'ajout au tableau:
    orchestra[i++] = new Wind();
    orchestra[i++] = new Percussion();
    orchestra[i++] = new Stringed();
    orchestra[i++] = new Brass();
    orchestra[i++] = new Woodwind();
    tuneAll(orchestra);
  }
} ///:~

Vous pouvez voir qu'il n'y a vraiment aucun changement excepté dans la classe de base.

Il est utile de créer des classes et des méthodes abstraites parce qu'elles forment l'abstraction d'une classe explicite et indique autant à utilisateur qu'au compilateur comment elles doivent être utilisées.

Constructeurs et polymorphisme

Comme d'habitude, les constructeurs se comportent différemment des autres sortes de méthodes. C'est encore vrai pour le polymorphisme. Quoique les constructeurs ne soient pas polymorphes (bien que vous puissiez avoir un genre de "constructeur virtuel", comme vous le verrez dans le chapitre 12), il est important de comprendre comment les constructeurs se comportent dans des hiérarchies complexes combiné avec le polymorphisme. Cette compréhension vous aidera a éviter de désagréables plats de nouilles.

Ordre d'appel des constructeurs

L'ordre d'appel des constructeurs a été brièvement discuté dans le chapitre 4 et également dans le chapitre 6, mais c'était avant l'introduction du polymorphisme.

Un constructeur de la classe de base est toujours appelé dans le constructeur d'une classe dérivée, en remontant la hiérarchie d'héritage de sorte qu'un constructeur pour chaque classe de base est appelé. Ceci semble normal car le travail du constructeur est précisément de construire correctement l'objet. Une classe dérivée a seulement accès à ses propres membres, et pas à ceux de la classe de base (dont les membres sont en général private). Seul le constructeur de la classe de base a la connaissance et l'accès appropriés pour initialiser ses propres éléments. Par conséquent, il est essentiel que tous les constructeurs soient appelés, sinon l'objet ne serait pas entièrement construit. C'est pourquoi le compilateur impose un appel de constructeur pour chaque partie d'une classe dérivée. Il appellera silencieusement le constructeur par défaut si vous n'appelez pas explicitement un constructeur de la classe de base dans le corps du constructeur de la classe dérivée. S' il n'y a aucun constructeur de défaut, le compilateur le réclamera (dans le cas où une classe n'a aucun constructeur, le compilateur générera automatiquement un constructeur par défaut).

Prenons un exemple qui montre les effets de la composition, de l'héritage, et du polymorphisme sur l'ordre de construction :


//: c07:Sandwich.java
// Ordre d'appel des constructeurs.
class Meal {
  Meal() { System.out.println("Meal()"); }
}

class Bread {
  Bread() { System.out.println("Bread()"); }
}

class Cheese {
  Cheese() { System.out.println("Cheese()"); }
}

class Lettuce {
  Lettuce() { System.out.println("Lettuce()"); }
}

class Lunch extends Meal {
  Lunch() { System.out.println("Lunch()");}
}

class PortableLunch extends Lunch {
  PortableLunch() {
    System.out.println("PortableLunch()");
  }
}

class Sandwich extends PortableLunch {
  Bread b = new Bread();
  Cheese c = new Cheese();
  Lettuce l = new Lettuce();
  Sandwich() {
    System.out.println("Sandwich()");
  }
  public static void main(String[] args) {
    new Sandwich();
  }
} ///:~

Cet exemple utilise une classe complexe et d'autres classes, chaque classe a un constructeur qui s'annonce lui-même. La classe importante est Sandwich, qui est au troisième niveau d'héritage (quatre, si vous comptez l'héritage implicite de Object) et qui a trois objets membres. Quand un objet Sandwich est créé dans le main(), l'output est :


Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()

Ceci signifie que l'ordre d'appel des constructeurs pour un objet complexe est le suivant :

  1. Le constructeur de la classe de base est appelé. Cette étape est répétée récursivement jusqu'à ce que la racine de la hiérarchie soit construite d'abord, suivie par la classe dérivée suivante, etc, jusqu'à atteindre la classe la plus dérivée.
  2. Les initialiseurs des membres sont appelés dans l'ordre de déclaration
  3. Le corps du constructeur de la classe dérivée est appelée.

L'ordre d'appel des constructeurs est important. Quand vous héritez, vous savez tout au sujet de la classe de base et pouvez accéder à tous les membres public et protected de la classe de base. Ceci signifie que vous devez pouvoir présumer que tous les membres de la classe de base sont valides quand vous êtes dans la classe dérivée. Dans une méthode normale, la construction a déjà eu lieu, ainsi tous les membres de toutes les parties de l'objet ont été construits. Dans le constructeur, cependant, vous devez pouvoir supposer que tous les membres que vous utilisez ont été construits. La seule manière de le garantir est d'appeler d'abord le constructeur de la classe de base. Ainsi, quand êtes dans le constructeur de la classe dérivée, tous les membres que vous pouvez accéder dans la classe de base ont été initialisés. Savoir que tous les membres sont valides à l'intérieur du constructeur est également la raison pour laquelle, autant que possible, vous devriez initialiser tous les objets membres (c'est à dire les objets mis dans la classe par composition) à leur point de définition dans la classe (par exemple, b, c, et l dans l'exemple ci-dessus). Si vous suivez cette recommandation, vous contribuerez à vous assurer que tous les membres de la classe de base et les objets membres de l'objet actuel aient été initialisés. Malheureusement, cela ne couvre pas tous les cas comme vous allez le voir dans le paragraphe suivant.

La méthode finalize() et l'héritage

Quand vous utilisez la composition pour créer une nouvelle classe, vous ne vous préoccupez pas de l'achèvement des objets membres de cette classe. Chaque membre est un objet indépendant et traité par le garbage collector indépendamment du fait qu'il soit un membre de votre classe. Avec l'héritage, cependant, vous devez redéfinir finalize() dans la classe dérivée si un nettoyage spécial doit être effectué pendant la phase de garbage collection. Quand vous redéfinissez finalize() dans une classe fille, il est important de ne pas oublier d'appeler la version de finalize() de la classe de base, sinon l'achèvement de la classe de base ne se produira pas. L'exemple suivant le prouve :


//: c07:Frog.java
// Test de la méthode finalize avec l'héritage.

class DoBaseFinalization {
  public static boolean flag = false;
}

class Characteristic {
  String s;
  Characteristic(String c) {
    s = c;
    System.out.println(
      "Creating Characteristic " + s);
  }
  protected void finalize() {
    System.out.println(
      "finalizing Characteristic " + s);
  }
}

class LivingCreature {
  Characteristic p =
    new Characteristic("is alive");
  LivingCreature() {
    System.out.println("LivingCreature()");
  }
  protected void finalize() {
    System.out.println(
      "LivingCreature finalize");
    // Appel de la version de la classe de base, à la fin!
    if(DoBaseFinalization.flag)
      try {
        super.finalize();
      } catch(Throwable t) {}
  }
}

class Animal extends LivingCreature {
  Characteristic p =
    new Characteristic("has heart");
  Animal() {
    System.out.println("Animal()");
  }
  protected void finalize() {
    System.out.println("Animal finalize");
    if(DoBaseFinalization.flag)
      try {
        super.finalize();
      } catch(Throwable t) {}
  }
}

class Amphibian extends Animal {
  Characteristic p =
    new Characteristic("can live in water");
  Amphibian() {
    System.out.println("Amphibian()");
  }
  protected void finalize() {
    System.out.println("Amphibian finalize");
    if(DoBaseFinalization.flag)
      try {
        super.finalize();
      } catch(Throwable t) {}
  }
}

public class Frog extends Amphibian {
  Frog() {
    System.out.println("Frog()");
  }
  protected void finalize() {
    System.out.println("Frog finalize");
    if(DoBaseFinalization.flag)
      try {
        super.finalize();
      } catch(Throwable t) {}
  }
  public static void main(String[] args) {
    if(args.length != 0 &&
       args[0].equals("finalize"))
       DoBaseFinalization.flag = true;
    else
          System.out.println("not finalizing bases");
    new Frog(); // Devient instantanément récupérable par le garbage collector
    System.out.println("bye!");
    // Force l'appel des finalisations:
    System.gc();
  }
} ///:~

Ce livre a été écrit par Bruce Eckel ( télécharger la version anglaise : Thinking in java )
Ce chapitre a été traduit par Jérome Dannoville ( groupe de traduction )
télécharger la version francaise (PDF) | Commandez le livre en version anglaise (amazon) | télécharger la version anglaise
pages : 1 2 3 4 
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java