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 8 - Interfaces et classes internes

pages : 1 2 3 4 5 6 

Si le problème de « l'héritage multiple d'implémentations » ne se pose pas, on peut tout à fait se passer des classes internes. Mais les classes internes fournissent toutefois des fonctionnalités intéressantes :

  1. Les classes internes peuvent avoir plusieurs instances, chacune avec ses propres informations indépendantes des informations de l'objet de la classe externe.
  2. Dans une classe externe on peut avoir plusieurs classes internes, chacune implémentant la même interface ou dérivant la même classe d'une façon différente. Nous allons en voir un exemple bientôt.
  3. Le point de création d'un objet de la classe interne n'est pas lié à la création de l'objet de la classe externe.
  4. Aucune confusion concernant la relation « est-un » n'est possible avec la classe interne ; c'est une entité séparée.

Par exemple, si Sequence.java n'utilisait pas de classes internes, il aurait fallu dire « une Sequence est un Selector », et on n'aurait pu avoir qu'un seul Selector pour une Sequence particulière. De plus, on peut avoir une autre méthode, getRSelector(), qui produise un Selector parcourant la Sequence dans l'ordre inverse. Cette flexibilité n'est possible qu'avec les classes internes.

Fermetures & callbacks

Une fermeture est un objet qui retient des informations de la portée dans laquelle il a été créé. A partir de cette définition, il est clair qu'une classe interne est une fermeture orientée objet, parce qu'elle ne contient pas seulement chaque élément d'information de l'objet de la classe externe (« la portée dans laquelle il a été créé »), mais elle contient aussi automatiquement une référence sur l'objet de la classe externe, avec la permission d'en manipuler tous les membres, y compris les private.

L'un des arguments les plus percutants mis en avant pour inclure certains mécanismes de pointeur dans Java était de permettre les callbacks. Avec un callback, on donne des informations à un objet lui permettant de revenir plus tard dans l'objet originel. Ceci est un concept particulièrement puissant, comme nous le verrons dans les chapitres 13 et 16. Cependant, si les callbacks étaient implémentés avec des pointeurs, le programmeur serait responsable de la gestion de ce pointeur et devrait faire attention afin de ne pas l'utiliser de manière incontrôlée. Mais comme on l'a déjà vu, Java n'aime pas ce genre de solutions reposant sur le programmeur, et les pointeurs ne furent pas inclus dans le langage.

Les classes internes fournissent une solution parfaite pour les fermetures, bien plus flexible et de loin plus sûre qu'un pointeur. Voici un exemple simple :

//: c08:Callbacks.java
// Utilisation des classes internes pour les callbacks

interface Incrementable {
  void increment();
}

// Il est très facile d'implémenter juste l'interface :
class Callee1 implements Incrementable {
  private int i = 0;
  public void increment() {
    i++;
    System.out.println(i);
  }
}

class MyIncrement {
  public void increment() {
    System.out.println("Other operation");
  }
  public static void f(MyIncrement mi) {
    mi.increment();
  }
}

// Si la classe doit aussi implémenter increment() d'une
// autre façon, il faut utiliser une classe interne :
class Callee2 extends MyIncrement {
  private int i = 0;
  private void incr() {
    i++;
    System.out.println(i);
  }
  private class Closure implements Incrementable {

    public void increment() { incr(); }
  }
  Incrementable getCallbackReference() {
    return new Closure();
  }
}

class Caller {
  private Incrementable callbackReference;
  Caller(Incrementable cbh) {
    callbackReference = cbh;
  }
  void go() {
    callbackReference.increment();
  }
}

public class Callbacks {
  public static void main(String[] args) {
    Callee1 c1 = new Callee1();
    Callee2 c2 = new Callee2();
    MyIncrement.f(c2);
    Caller caller1 = new Caller(c1);
    Caller caller2 =
      new Caller(c2.getCallbackReference());
    caller1.go();
    caller1.go();
    caller2.go();
    caller2.go();
  }
} ///:~

Cet exemple est un exemple supplémentaire montrant les différences entre l'implémentation d'une interface dans une classe externe ou une classe interne. Callee1 est sans conteste la solution la plus simple en terme de code. Callee2 hérite de MyIncrement qui dispose déjà d'une méthode increment() faisant quelque chose de complètement différent que ce qui est attendu par l'interface Incrementable. Quand MyIncrement est dérivée dans Callee2, increment() ne peut être redéfinie pour être utilisée par Incrementable, on est donc forcé d'utiliser une implémentation séparée avec une classe interne. Notez également que lorsqu'on crée une classe interne, on n'étend pas ni ne modifie l'interface de la classe externe.

Remarquez bien que tout dans Callee2 à l'exception de getCallbackReference() est private. L'interface Incrementable est essentielle pour permettre toute interaction avec le monde extérieur. Les interfaces permettent donc une séparation complète entre l'interface et l'implémentation.

La classe interne Closure implémente Incrementable uniquement pour fournir un point de retour dans Callee2 - mais un point de retour sûr. Quiconque récupère la référence sur Incrementable ne peut appeler qu'increment() (contrairement à un pointeur, qui aurait permis de faire tout ce qu'on veut).

Caller prend une référence Incrementable dans son constructeur (bien qu'on puisse fournir cette référence - ce callback - n'importe quand), et s'en sert par la suite, parfois bien plus tard, pour « revenir » dans la classe Callee.

La valeur des callbacks réside dans leur flexibilité - on peut décider dynamiquement quelles fonctions vont être appelées lors de l'exécution. Les avantages des callbacks apparaîtront dans le chapitre 13, où ils sont utilisés immodérément pour implémenter les interfaces graphiques utilisateurs (GUI).

Classes internes & structures de contrôle

Un exemple plus concret d'utilisation des classes internes est ce que j'appelle les structures de contrôle.

Une structure d'application est une classe ou un ensemble de classes conçues pour résoudre un type particulier de problème. Pour utiliser une structure d'application, il suffit de dériver une ou plusieurs de ces classes et de redéfinir certaines des méthodes. Le code écrit dans les méthodes redéfinies particularise la solution générale fournie par la structure d'application, afin de résoudre le problème considéré. Les structures de contrôle sont un type particulier des structures d'application dominées par la nécessité de répondre à des événements ; un système qui répond à des événements est appelé un système à programmation événementielle. L'un des problèmes les plus ardus en programmation est l'interface graphique utilisateur (GUI), qui est quasiment entièrement événementielle. Comme nous le verrons dans le Chapitre 13, la bibliothèque Java Swing est une structure de contrôle qui résoud élégamment le problème des interfaces utilisateurs en utilisant extensivement les classes internes.

Pour voir comment les classes internes permettent une mise en oeuvre aisée des structures de contrôle, considérons le cas d'une structure de contrôle dont le rôle consiste à exécuter des événements dès lors que ces événements sont « prêts ». Bien que « prêt » puisse vouloir dire n'importe quoi, dans notre cas nous allons nous baser sur un temps d'horloge. Ce qui suit est une structure de contrôle qui ne contient aucune information spécifique sur ce qu'elle contrôle. Voici tout d'abord l'interface qui décrit tout événement. C'est une classe abstract plutôt qu'une interface parce que le comportement par défaut est de réaliser le contrôle sur le temps, donc une partie de l'implémentation peut y être incluse :

//: c08:controller:Event.java
// Les méthodes communes pour n'importe quel événement.
package c08.controller;

abstract public class Event {
  private long evtTime;
  public Event(long eventTime) {
    evtTime = eventTime;
  }
  public boolean ready() {
    return System.currentTimeMillis() >= evtTime;
  }
  abstract public void action();
  abstract public String description();
} ///:~

Le constructeur stocke simplement l'heure à laquelle on veut que l'Event soit exécuté, tandis que ready() indique si c'est le moment de le lancer. Bien sûr, ready() peut être redéfini dans une classe dérivée pour baser les Event sur autre chose que le temps.

action() est la méthode appelée lorsque l'Event est ready(), et description() donne des informations (du texte) à propos de l'Event.

Le fichier suivant contient la structure de contrôle proprement dite qui gère et déclenche les événements. La première classe est simplement une classe « d'aide » dont le rôle consiste à stocker des objets Event. On peut la remplacer avec n'importe quel conteneur plus approprié, et dans le Chapitre 9 nous verrons d'autres conteneurs qui ne requerront pas ce code supplémentaire :

//: c08:controller:Controller.java
// Avec Event, la structure générique
// pour tous les systèmes de contrôle :
package c08.controller;

// Ceci est jsute une manière de stocker les objets Event.
class EventSet {
  private Event[] events = new Event[100];
  private int index = 0;
  private int next = 0;
  public void add(Event e) {
    if(index >= events.length)
      return; // (Normalement, générer une exception)
    events[index++] = e;
  }
  public Event getNext() {
    boolean looped = false;
    int start = next;
    do {
      next = (next + 1) % events.length;
      // Vérifie si on a fait le tour :
      if(start == next) looped = true;
      // Si on a fait le tour, c'est que la
      // liste est vide :
      if((next == (start + 1) % events.length)
         && looped)
        return null;
    } while(events[next] == null);
    return events[next];
  }
  public void removeCurrent() {
    events[next] = null;
  }
}

public class Controller {
  private EventSet es = new EventSet();
  public void addEvent(Event c) { es.add(c); }
  public void run() {
    Event e;
    while((e = es.getNext()) != null) {
      if(e.ready()) {
        e.action();
        System.out.println(e.description());
        es.removeCurrent();
      }
    }
  }
} ///:~

EventSet stocke arbitrairement 100 Events (si un « vrai » conteneur du Chapitre 9 était utilisé ici, on n'aurait pas à se soucier à propos de sa taille maximum, puisqu'il se redimensionnerait de lui-même). L'index est utilisé lorsqu'on veut récupérer le prochain Event de la liste, pour voir si on a fait le tour. Ceci est important pendant un appel à getNext(), parce que les objets Event sont enlevés de la liste (avec removeCurrent()) une fois exécutés, donc getNext() rencontrera des trous dans la liste lorsqu'il la parcourra.

Notez que removeCurrent() ne positionne pas simplement un flag indiquant que l'objet n'est plus utilisé. A la place, il positionne la référence à null. C'est important car si le ramasse-miettes rencontre une référence qui est encore utilisée il ne pourra pas nettoyer l'objet correspondant. Si l'objet n'a plus de raison d'être (comme c'est le cas ici), il faut alors mettre leur référence à null afin d'autoriser le ramasse-miettes à les nettoyer.

C'est dans Controller que tout le travail est effectué. Il utiliser un EventSet pour stocker ses objets Event, et addEvent() permet d'ajouter de nouveaux éléments à la liste. Mais la méthode principale est run(). Cette méthode parcourt l'EventSet, recherchant un objet Event qui soit ready(). Il appelle alors la méthode action() pour cet objet, affiche sa description() et supprime l'Event de la liste.

Notez que jusqu'à présent dans la conception on ne sait rien sur ce que fait exactement un Event. Et c'est le point fondamental de la conception : comment elle « sépare les choses qui changent des choses qui ne bougent pas ». Ou, comme je l'appelle, le « vecteur de changement » est constitué des différentes actions des différents types d'objets Event, actions différentes réalisées en créant différentes sous-classes d'Event.

C'est là que les classes internes interviennent. Elles permettent deux choses :

  1. Réaliser l'implémentation complète d'une application de structure de contrôle dans une seule classe, encapsulant du même coup tout ce qui est unique dans cette implémentation. Les classes internes sont utilisées pour décrire les différents types d'action() nécessaires pour résoudre le problème. De plus, l'exemple suivant utilise des classes internes private afin que l'implémentation soit complètement cachée et puisse être changée en toute impunité.
  2. Empêcher que l'implémentation ne devienne trop lourde, puisqu'on est capable d'accéder facilement à chacun des membres de la classe externe. Sans cette facilité, le code deviendrait rapidement tellement confus qu'il faudrait chercher une autre solution.

Considérons une implémentation particulière de la structure de contrôle conçue pour contrôler les fonctions d'une serre  [43]. Chaque action est complètement différente : contrôler les lumières, l'arrosage et la température, faire retentir des sonneries et relancer le système. Mais la structure de contrôle est conçue pour isoler facilement ce code différent. Les classes internes permettent d'avoir de multiples versions dérivées de la même classe de base (ici, Event) à l'intérieur d'une seule et même classe. Pour chaque type d'action on crée une nouvelle classe interne dérivée d'Event, et on écrit le code de contrôle dans la méthode action().

Typiquement, la classe GreenhouseControls hérite de Controller :

//: c08:GreenhouseControls.java
// Ceci est une application spécifique du
// système de contrôle, le tout dans une seule classe.
// Les classes internes permettent d'encapsuler des
// fonctionnalités différentes pour chaque type d'Event.
import c08.controller.*;

public class GreenhouseControls
    extends Controller {
  private boolean light = false;
  private boolean water = false;
  private String thermostat = "Day";
  private class LightOn extends Event {
    public LightOn(long eventTime) {
      super(eventTime);
    }
    public void action() {
      // Placer ici du code de contrôle hardware pour
      // réellement allumer la lumière.
      light = true;
    }
    public String description() {
      return "Light is on";
    }
  }
  private class LightOff extends Event {
    public LightOff(long eventTime) {
      super(eventTime);
    }
    public void action() {
      // Put hardware control code here to
      // physically turn off the light.
      light = false;
    }
    public String description() {
      return "Light is off";
    }
  }
  private class WaterOn extends Event {
    public WaterOn(long eventTime) {
      super(eventTime);
    }
    public void action() {
      // Placer ici du code de contrôle hardware.
      water = true;
    }
    public String description() {
      return "Greenhouse water is on";
    }
  }
  private class WaterOff extends Event {
    public WaterOff(long eventTime) {
      super(eventTime);
    }
    public void action() {
      // Placer ici du code de contrôle hardware.
      water = false;
    }
    public String description() {
      return "Greenhouse water is off";
    }
  }
  private class ThermostatNight extends Event {
    public ThermostatNight(long eventTime) {
      super(eventTime);
    }
    public void action() {
      // Placer ici du code de contrôle hardware.
      thermostat = "Night";
    }
    public String description() {
      return "Thermostat on night setting";
    }
  }
  private class ThermostatDay extends Event {
    public ThermostatDay(long eventTime) {
      super(eventTime);
    }
    public void action() {
      // Placer ici du code de contrôle hardware.
      thermostat = "Day";
    }
    public String description() {
      return "Thermostat on day setting";
    }
  }
  // Un exemple d'une action() qui insère une nouvelle
  // instance de son type dans la liste d'Event :
  private int rings;
  private class Bell extends Event {
    public Bell(long eventTime) {
      super(eventTime);
    }
    public void action() {
      // Sonne toutes les 2 secondes, 'rings' fois :
      System.out.println("Bing!");
      if(--rings > 0)
        addEvent(new Bell(
          System.currentTimeMillis() + 2000));
    }
    public String description() {
      return "Ring bell";
    }
  }
  private class Restart extends Event {
    public Restart(long eventTime) {
      super(eventTime);
    }
    public void action() {
      long tm = System.currentTimeMillis();
      // Au lieu d'un codage en dur, on pourrait
      // récupérer les informations en parsant un
      // fichier de configuration :
      rings = 5;
      addEvent(new ThermostatNight(tm));
      addEvent(new LightOn(tm + 1000));
      addEvent(new LightOff(tm + 2000));
      addEvent(new WaterOn(tm + 3000));
      addEvent(new WaterOff(tm + 8000));
      addEvent(new Bell(tm + 9000));
      addEvent(new ThermostatDay(tm + 10000));
      // On peut même ajouter un objet Restart !
      addEvent(new Restart(tm + 20000));
    }
    public String description() {
      return "Restarting system";
    }
  }
  public static void main(String[] args) {
    GreenhouseControls gc =
      new GreenhouseControls();
    long tm = System.currentTimeMillis();
    gc.addEvent(gc.new Restart(tm));
    gc.run();
  }
} ///:~

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 Quelin ( 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 5 6 
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