IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Thinking in Java, 3rd ed. Revision 4.0


précédentsommairesuivant

VIII. Interfaces et classes internes

Les interfaces et les classes internes sont des manières plus sophistiquées d'organiser et de contrôler les objets du système construit.

C++, par exemple, ne propose pas ces mécanismes, bien que le programmeur expérimenté puisse les simuler. Le fait qu'ils soient présents dans Java indique qu'ils furent considérés comme assez importants pour être intégrés directement grâce à des mots-clefs.

Dans le chapitre 7, nous avons vu le mot-clef abstract, qui permet de créer une ou plusieurs méthodes dans une classe qui n'ont pas de définition - on fournit une partie de l'interface sans l'implémentation correspondante, qui est créée par ses héritiers. Le mot-clef interface produit une classe complètement abstraite, qui ne fournit absolument aucune implémentation. Nous verrons qu'une interface est un peu plus qu'une classe abstraite poussée à l'extrême, puisqu'elle permet d'implémenter « l'héritage multiple » du C++ en créant une classe qui peut être transtypée en plus d'un type de base.

Les classes internes ressemblent au premier abord à un simple mécanisme de dissimulation de code : on crée une classe à l'intérieur d'autres classes. Cependant, les classes internes font plus que cela - elles connaissent et peuvent communiquer avec la classe principale - ; sans compter que le code produit en utilisant les classes internes est plus élégant et compréhensible, bien que ce soit un concept nouveau pour beaucoup. Cela prend un certain temps avant d'intégrer les classes internes dans la conception.

VIII-A. Interfaces

Le mot-clef interface pousse le concept abstract un cran plus loin. On peut y penser comme à une classe « purement » abstract. Il permet au créateur d'établir la forme qu'aura la classe : les noms des méthodes, les listes d'arguments et les types de retours, mais pas les corps des méthodes. Une interface peut aussi contenir des données membres, mais elles seront implicitement static et final. Une interface fournit un patron pour la classe, mais aucune implémentation.

Une interface déclare : « Voici ce à quoi ressembleront toutes les classes qui implémenteront cette interface ». Ainsi, tout code utilisant une interface particulière sait quelles méthodes peuvent être appelées pour cette interface, et c'est tout. Une interface est donc utilisée pour établir un « protocole » entre les classes (certains langages de programmation orientés objet ont un mot-clef protocol pour réaliser la même chose).

Pour créer une interface, utilisez le mot-clef interface à la place du mot-clef class. Comme pour une classe, on peut ajouter le mot-clef public devant le mot-clef interface (mais seulement si l'interface est définie dans un fichier du même nom) ou ne rien mettre pour lui donner le statut « amical » afin qu'elle ne soit utilisable que dans le même package.

Le mot-clef implements permet de rendre une classe conforme à une interface particulière (ou à un groupe d'interfaces). Il dit en gros : « L'interface spécifie ce à quoi la classe ressemble, mais maintenant on va spécifier comment cela fonctionne ». Sinon, cela s'apparente à de l'héritage. Le diagramme des instruments de musique suivant le montre :

TIJ322.png

Vous pouvez constater d'après les classes Woodwind et Brass qu’une fois une interface implémentée, cette implémentation devient une classe ordinaire qui peut être étendue d'une façon tout à fait classique.

On peut choisir de déclarer explicitement les méthodes d'une interface comme public. Mais elles sont public même sans le préciser. C'est pourquoi il faut définir les méthodes d'une interface comme public quand on implémente une interface. Autrement elles sont « amicales » par défaut, impliquant une réduction de l'accessibilité d'une méthode durant l'héritage, ce qui est interdit par le compilateur Java.

On peut le voir dans cette version modifiée de l'exemple Instrument. Notons que chaque méthode de l'interface n'est strictement qu'une déclaration, la seule chose que le compilateur permette. De plus, aucune des méthodes d'Instrument n'est déclarée comme public, mais elles le sont automatiquement.

 
Sélectionnez
//: c08:music5:Music5.java
// Interfaces.
import java.util.*;

interface Instrument {
  // Constante compilée :
  int i = 5; // static & final
  // Définitions de méthodes interdites :
  void play(); // Automatiquement public
  String what();
  void adjust();
}

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

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

class Stringed implements 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 Music5 {
  // Le type n'est pas important, donc les nouveaux
  // types ajoutés au système marchent sans problème :
  static void tune(Instrument i) {
    // ...
    i.play();
  }
  static void tuneAll(Instrument[] e) {
    for(int i = 0; i < e.length; i++)
      tune(e[i]);
  }
  public static void main(String[] args) {
    Instrument[] orchestra = new Instrument[5];
    int i = 0;
    // Transtypage ascendant durant le stockage dans le tableau :
    orchestra[i++] = new Wind();
    orchestra[i++] = new Percussion();
    orchestra[i++] = new Stringed();
    orchestra[i++] = new Brass();
    orchestra[i++] = new Woodwind();
    tuneAll(orchestra);
  }
} ///:~

Le reste du code fonctionne de la même manière. Peu importe si vous « upcastez » à une classe « régulière » appelée Instrument, une classe abstract Instrument, ou à une interface Instrument. Le comportement est le même. En fait, vous pouvez voir dans la méthode tune() qu'il n'y a pas de précisions sur le fait que Instrument soit une classe « régulière », une classe abstract, ou une interface. Voici le principe : chaque approche permet au programmeur de contrôler la façon dont les objets sont créés et utilisés.

VIII-A-1. « Héritage multiple » en Java

Une interface n'est pas simplement une forme « plus pure » d'une classe abstract. Elle a un but plus important que cela. Puisqu'une interface ne dispose d'aucune implémentation - autrement dit, aucun stockage n'est associé à une interface -, rien n'empêche de combiner plusieurs interfaces. Ceci est intéressant, car certaines fois on a la relation « Un x est un a et un b et un c ». En C++, le fait de combiner les interfaces de plusieurs classes est appelé héritage multiple, et entraîne une lourde charge du fait que chaque classe peut avoir sa propre implémentation. En Java, on peut réaliser la même chose, mais une seule classe peut avoir une implémentation, donc les problèmes rencontrés en C++ n'apparaissent pas en Java lorsqu'on combine les interfaces multiples :

TIJ323.png

Dans une classe dérivée, on n'est pas forcé d'avoir une classe de base qui soit abstract ou « concrète » (i.e. sans méthode abstract). Si une classe hérite d'une classe qui n'est pas une interface, elle ne peut dériver que de cette seule classe. Tous les autres types de base doivent être des interfaces. On place les noms des interfaces après le mot-clef implements en les séparant par des virgules. On peut spécifier autant d'interfaces qu'on veut - chacune devient un type indépendant vers lequel on peut transtyper. L'exemple suivant montre une classe concrète combinée à plusieurs interfaces pour produire une nouvelle classe :

 
Sélectionnez
//: c08:Adventure.java
// Interfaces multiples.
import java.util.*;

interface CanFight {
  void fight();
}

interface CanSwim {
  void swim();
}

interface CanFly {
  void fly();
}

class ActionCharacter {
  public void fight() {}
}

class Hero extends ActionCharacter
    implements CanFight, CanSwim, CanFly {
  public void swim() {}
  public void fly() {}
}

public class Adventure {
  static void t(CanFight x) { x.fight(); }
  static void u(CanSwim x) { x.swim(); }
  static void v(CanFly x) { x.fly(); }
  static void w(ActionCharacter x) { x.fight(); }
  public static void main(String[] args) {
    Hero h = new Hero();
    t(h); // Le traite comme un CanFight
    u(h); // Le traite comme un CanSwim
    v(h); // Le traite comme un CanFly
    w(h); // Le traite comme un ActionCharacter
  }
} ///:~

Ici, Hero combine la classe concrète ActionCharacter avec les interfaces CanFight, CanSwim et CanFly. Quand on combine une classe concrète avec des interfaces de cette manière, la classe concrète doit être spécifiée en premier, avant les interfaces (autrement le compilateur génère une erreur).

Notons que la signature de fight() est la même dans l'interface CanFight et dans la classe ActionCharacter, et que Hero ne fournit pas de définition pour fight(). On peut hériter d'une interface (comme on va le voir bientôt), mais dans ce cas on a une autre interface. Si on veut créer un objet de ce nouveau type, ce doit être une classe implémentant toutes les définitions. Bien que la classe Hero ne fournisse pas explicitement une définition pour fight(), la définition est fournie par ActionCharacter, donc héritée par Hero et il est ainsi possible de créer des objets Hero.

Dans la classe Adventure, on peut voir quatre méthodes prenant les diverses interfaces et la classe concrète en argument. Quand un objet Hero est créé, il peut être utilisé dans chacune de ces méthodes, ce qui veut dire qu'il est transtypé tour à tour dans chaque interface. De la façon dont cela est conçu en Java, cela fonctionne sans problème et sans effort supplémentaire de la part du programmeur.

L'intérêt principal des interfaces est démontré dans l'exemple précédent : être capable de transtyper vers plus d'un type de base. Cependant, une seconde raison, la même que pour les classes de base abstract, plaide pour l'utilisation des interfaces : empêcher le programmeur client de créer un objet de cette classe et spécifier qu'il ne s'agit que d'une interface. Cela soulève une question : faut-il utiliser une interface ou une classe abstract ? Une interface apporte les bénéfices d'une classe abstract et les bénéfices d'une interface, donc s'il est possible de créer la classe de base sans définir de méthodes ou de données membres, il faut toujours préférer les interfaces aux classes abstract. En fait, si on sait qu'un type sera amené à être dérivé, il faut le créer d'emblée comme une interface, et ne le changer en classe abstract, voire en classe concrète, que si on est forcé d'y placer des définitions de méthodes ou des données membres.

VIII-A-1-a. Combinaison d'interfaces et collisions de noms

On peut rencontrer un problème lorsqu'on implémente plusieurs interfaces. Dans l'exemple précédent, CanFight et ActionCharacter ont tous les deux une méthode void fight() identique. Cela ne pose pas de problèmes parce que la méthode est identique dans les deux cas, mais que se passe-t-il lorsque ce n'est pas le cas ? Voici un exemple :

 
Sélectionnez
//: c08:InterfaceCollision.java

interface I1 { void f(); }
interface I2 { int f(int i); }
interface I3 { int f(); }
class C { public int f() { return 1; } }

class C2 implements I1, I2 {
  public void f() {}
  public int f(int i) { return 1; } // surchargée
}

class C3 extends C implements I2 {
  public int f(int i) { return 1; } // surchargée
}

class C4 extends C implements I3 {
  // Identique, pas de problème :
  public int f() { return 1; }
}

// Les méthodes diffèrent seulement par le type de retour :
//! class C5 extends C implements I1 {}
//! interface I4 extends I1, I3 {} ///:~

Les difficultés surviennent parce que la redéfinition, l'implémentation et la surcharge sont toutes les trois utilisées ensemble, et que les fonctions surchargées ne peuvent différer seulement par leur type de retour. Quand les deux dernières lignes sont décommentées, le message d'erreur est explicite :

InterfaceCollision.java:23: f( ) in C cannot implement f( ) in I1; attempting to use incompatible return type
found : int
required: void
InterfaceCollision.java:24: interfaces I3 and I1 are incompatible; both define f( ), but with different return type

De toute façon, utiliser les mêmes noms de méthode dans différentes interfaces destinées à être combinées affecte la compréhension du code. Tachez donc de l'éviter.

VIII-A-2. Étendre une interface avec l'héritage

On peut facilement ajouter de nouvelles déclarations de méthodes à une interface en la dérivant, de même qu'on peut combiner plusieurs interfaces dans une nouvelle interface grâce à l'héritage. Dans les deux cas on a une nouvelle interface, comme dans l'exemple suivant :

 
Sélectionnez
//: c08:HorrorShow.java
// Extension d'une interface grâce à l'héritage.

interface Monster {
    void menace();
}

interface DangerousMonster extends Monster {
    void destroy();
}

interface Lethal {
    void kill();
}

class DragonZilla implements DangerousMonster {
    public void menace() {}
    public void destroy() {}
}

interface Vampire extends DangerousMonster, Lethal {
    void drinkBlood();
}

class VeryBadVampire implements Vampire {
    public void menace() {}
    public void destroy() {}
    public void kill() {}
    public void drinkBlood() {}
}

public class HorrorShow {
    static void u(Monster b) { b.menace(); }
    static void v(DangerousMonster d) {
        d.menace();
        d.destroy();
    }
    static void w(Lethal l) { l.kill(); }
    public static void main(String[] args) {
        DangerousMonster barney = new DragonZilla();
        u(barney);
        v(barney);
        Vampire vlad = new VeryBadVampire();
        u(vlad);
        v(vlad);
        w(vlad);
    }
} ///:~

DangerousMonster est une simple extension de Monster qui fournit une nouvelle interface. Elle est implémentée dans DragonZilla.

La syntaxe utilisée dans Vampire n'est valide que lorsqu'on dérive des interfaces. Normalement, on ne peut utiliser extends qu'avec une seule classe, mais comme une interface peut être constituée de plusieurs autres interfaces, extends peut se référer à plusieurs interfaces de base lorsqu'on construit une nouvelle interface. Comme vous pouvez le voir, les noms d'interface sont simplement séparés par des virgules.

VIII-A-3. Groupes de constantes

Puisque toutes les données membres d'une interface sont automatiquement static et final, une interface est un outil pratique pour créer des groupes de constantes, un peu comme avec le enum du C ou du C++. Par exemple :

 
Sélectionnez
//: c08:Months.java
// Utiliser les interfaces pour créer des groupes de constantes.
package c08;

public interface Months {
    int
        JANUARY = 1, FEBRUARY = 2, MARCH = 3,
        APRIL = 4, MAY = 5, JUNE = 6, JULY = 7,
        AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10,
        NOVEMBER = 11, DECEMBER = 12;
} ///:~

Notons au passage l'utilisation des conventions de style Java pour les champs static finals initialisés par des constantes : rien que des majuscules (avec des underscores pour séparer les mots à l'intérieur d'un identifiant).

Les données membres d'une interface sont automatiquement public, il n'est donc pas nécessaire de le spécifier.

Maintenant on peut utiliser les constantes à l'extérieur du package en important c08.* ou c08.Months de la même manière qu'on le ferait avec n'importe quel autre package, et référencer les valeurs avec des expressions comme Months.JANUARY. Bien sûr, on ne récupère qu'un int, il n'y a donc pas de vérification additionnelle de type comme celle dont dispose l'enum du C++, mais cette technique (couramment utilisée) reste tout de même une grosse amélioration comparée aux nombres codés en dur dans les programmes (appelés « nombres magiques » et produisant un code pour le moins difficile à maintenir).

Si on veut une vérification additionnelle de type, on peut construire une classe de la manière suivante : (33)

 
Sélectionnez
//: c08:Month.java
// Un système d'énumération plus robuste.
package c08;
import com.bruceeckel.simpletest.*;

public final class Month {
    private static Test monitor = new Test();
    private String name;
    private Month(String nm) { name = nm; }
    public String toString() { return name; }
    public static final Month
        JAN = new Month("January"),
        FEB = new Month("February"),
        MAR = new Month("March"),
        APR = new Month("April"),
        MAY = new Month("May"),
        JUN = new Month("June"),
        JUL = new Month("July"),
        AUG = new Month("August"),
        SEP = new Month("September"),
        OCT = new Month("October"),
        NOV = new Month("November"),
        DEC = new Month("December");
    public static final Month[] month =  {
        JAN, FEB, MAR, APR, MAY, JUN,
        JUL, AUG, SEP, OCT, NOV, DEC
    };
    public static final Month number(int ord) {
        return month[ord - 1];
    }
    public static void main(String[] args) {
        Month m = Month.JAN;
        System.out.println(m);
        m = Month.number(12);
        System.out.println(m);
        System.out.println(m == Month.DEC);
        System.out.println(m.equals(Month.DEC));
        System.out.println(Month.month[3]);
        monitor.expect(new String[] {
            "January",
            "December",
            "true",
            "true",
            "April"
        });
    }
} ///:~

Month est une classe final avec un constructeur private, afin que personne ne puisse la dériver ou en faire une instance. Les seules instances sont celles static final créées dans la classe elle-même : JAN, FEB, MAR, etc. Ces objets sont aussi utilisés dans le tableau Month, ce qui vous donne la possibilité d'itérer sur un tableau d'objet Month. La méthode number( ) vous permet de sélectionner un Month en utilisant sont numéro de mois correspondant. Dans main() on dispose de la vérification additionnelle de type : m est un objet Month et ne peut donc se voir assigné qu'un Month. L'exemple précédent Months.java ne fournissait que des valeurs int, et donc une variable int destinée à représenter un mois pouvait en fait recevoir n'importe quelle valeur entière, ce qui n'était pas très sûr.

Cette approche nous permet aussi d'utiliser indifféremment == ou equals(), ainsi que le montre la fin de main(). Ceci fonctionne, car il ne peut y avoir qu'une seule instance de chaque valeur de Month. Dans le Chapitre 11, vous apprendrez une autre manière d'utiliser les classes afin de comparer les objets entre eux.

Il y a aussi un champ Month dans java.util.Calendar.

Le projet Apache Jakarta Commons possède un outil pour créer des énumérations de façon similaire à ce qui à été vu dans l'exemple précédent, mais avec mois d'effort. Voir http://jakarta.apache.org/commons, dans « lang, » dans le package org.apache.commons.lang.enum. Ce projet possède aussi de nombreuses autres librairies très utiles.

VIII-A-4. Initialisation des champs dans les interfaces

Les champs définis dans les interfaces sont automatiquement static et final. Ils ne peuvent être des « finals vides », mais peuvent être initialisés avec des expressions non constantes. Par exemple :

 
Sélectionnez
//: c08:RandVals.java
// Initialisation de champs d'interface
// avec des valeurs non constantes.
import java.util.*;

public interface RandVals {
    Random rand = new Random();
    int randomInt = rand.nextInt(10);
    long randomLong = rand.nextLong() * 10;
    float randomFloat = rand.nextLong() * 10;
    double randomDouble = rand.nextDouble() * 10;
} ///:~

Puisque les champs sont static, ils sont initialisés quand la classe est chargée pour la première fois, ce qui arrive quand n'importe lequel des champs est accédé pour la première fois. Voici un simple test :

 
Sélectionnez
//: c08:TestRandVals.java
import com.bruceeckel.simpletest.*;

public class TestRandVals {
    private static Test monitor = new Test();
    public static void main(String[] args) {
        System.out.println(RandVals.randomInt);
        System.out.println(RandVals.randomLong);
        System.out.println(RandVals.randomFloat);
        System.out.println(RandVals.randomDouble);
        monitor.expect(new String[] {
            "%% -?\\d+",
            "%% -?\\d+",
            "%% -?\\d\\.\\d+E?-?\\d+",
            "%% -?\\d\\.\\d+E?-?\\d+"
        });
    }
} ///:~

Les données membres, bien sûr, ne font pas partie de l'interface, mais sont stockées dans la zone de stockage static de cette interface.

VIII-A-5. Interfaces imbriquées

Les interfaces peuvent être imbriquées dans des classes ou à l'intérieur d'autres interfaces. (34) Ceci révèle nombre de fonctionnalités intéressantes :

 
Sélectionnez
//: c08:NestingInterfaces.java

class A {
  interface B {
    void f();
  }
  public class BImp implements B {
    public void f() {}
  }
  private class BImp2 implements B {
    public void f() {}
  }
  public interface C {
    void f();
  }
  class CImp implements C {
    public void f() {}
  }
  private class CImp2 implements C {
    public void f() {}
  }
  private interface D {
    void f();
  }
  private class DImp implements D {
    public void f() {}
  }
  public class DImp2 implements D {
    public void f() {}
  }
  public D getD() { return new DImp2(); }
  private D dRef;
  public void receiveD(D d) {
    dRef = d;
    dRef.f();
  }
}

interface E {
  interface G {
    void f();
  }
  // « public » est redondant :
  public interface H {
    void f();
  }
  void g();
  // Ne peut pas être private dans une interface :
  //! private interface I {}
}

public class NestingInterfaces {
  public class BImp implements A.B {
    public void f() {}
  }
  class CImp implements A.C {
    public void f() {}
  }
  // Ne peut pas implémenter une interface private sauf
  // à l'intérieur de la classe définissant cette interface :
  //! class DImp implements A.D {
  //!  public void f() {}
  //! }
  class EImp implements E {
    public void g() {}
  }
  class EGImp implements E.G {
    public void f() {}
  }
  class EImp2 implements E {
    public void g() {}
    class EG implements E.G {
      public void f() {}
    }
  }
  public static void main(String[] args) {
    À a = new A();
    // Ne peut accéder à A.D :
    //! A.D ad = a.getD();
    // Ne renvoie qu'un A.D :
    //! A.DImp2 di2 = a.getD();
    // Ne peut accéder à un membre de l'interface :
    //! a.getD().f();
    // Seul un autre A peut faire quelque chose avec getD() :
    À a2 = new A();
    a2.receiveD(a.getD());
  }
} ///:~

La syntaxe permettant d'imbriquer une interface à l'intérieur d'une classe est relativement évidente ; et comme les interfaces non imbriquées, elles peuvent avoir une visibilité public ou « amicale ». On peut aussi constater que les interfaces public et « amicales » peuvent être implémentées dans des classes imbriquées public, « amicales » ou private.

Une nouvelle astuce consiste à rendre les interfaces private comme A.D (la même syntaxe est utilisée pour la qualification des interfaces imbriquées et pour les classes imbriquées). À quoi sert une interface imbriquée private ? On pourrait penser qu'elle ne peut être implémentée que comme une classe private imbriquée comme DImp, mais A.DImp2 montre qu'elle peut aussi être implémentée dans une classe public. Cependant, A.DImp2 ne peut être utilisée que comme elle-même : on ne peut mentionner le fait qu'elle implémente l'interface private, et donc implémenter une interface private est une manière de forcer la définition des méthodes de cette interface sans ajouter aucune information de type (c'est-à-dire, sans autoriser de transtypage ascendant).

La méthode getD( ) se trouve quant à elle dans une impasse du fait de l'interface private : c'est une méthode public qui renvoie une référence à une interface private. Que peut-on faire avec la valeur de retour de cette méthode ? Dans main( ), on peut voir plusieurs tentatives pour utiliser cette valeur de retour, qui échouent toutes. La seule solution possible est lorsque la valeur de retour est gérée par un objet qui a la permission de l'utiliser - dans ce cas, un objet A, via la méthode receiveD( ).

L'interface E montre que les interfaces peuvent être imbriquées les unes dans les autres. Cependant, les règles portant sur les interfaces - en particulier celle stipulant que tous les éléments doivent être public - sont strictement appliquées, donc une interface imbriquée à l'intérieur d'une autre interface est automatiquement public et ne peut être déclarée private.

NestingInterfaces montre les différentes manières dont les interfaces imbriquées peuvent être implémentées. En particulier, il est bon de noter que lorsqu'on implémente une interface, on n'est pas obligé d'en implémenter les interfaces imbriquées. De plus, les interfaces private ne peuvent être implémentées en dehors de leur classe de définition.

On peut penser que ces fonctionnalités n'ont été introduites que pour assurer une cohérence syntaxique, mais j'ai remarqué qu'une fois qu'une fonctionnalité est connue, on découvre souvent des endroits où elle se révèle utile.

VIII-B. Classes internes

Il est possible de placer la définition d'une classe à l'intérieur de la définition d'une autre classe. C'est ce qu'on appelle une classe interne. Les classes internes sont une fonctionnalité importante du langage, car elles permettent de grouper les classes qui sont logiquement rattachées entre elles, et de contrôler la visibilité de l'une à partir de l'autre. Cependant, il est important de comprendre que le mécanisme des classes internes est complètement différent de celui de la composition.

Souvent, lorsqu'on en entend parler pour la première fois, l'intérêt des classes internes n'est pas immédiatement évident. À la fin de cette section, après avoir discuté de la syntaxe et de la sémantique des classes internes, vous trouverez des exemples qui devraient clairement montrer les bénéfices des classes internes.

Une classe interne est créée comme on pouvait s'y attendre - en plaçant la définition de la classe à l'intérieur d'une autre classe :

 
Sélectionnez
//: c08:Parcel1.java
// Création de classes internes.

public class Parcel1 {
  class Contents {
    private int i = 11;
    public int value() { return i; }
  }
  class Destination {
    private String label;
    Destination(String whereTo) {
      label = whereTo;
    }
    String readLabel() { return label; }
  }
  // L'utilisation d'une classe interne ressemble à
  // l'utilisation de n'importe quelle autre classe depuis Parcell :
  public void ship(String dest) {
    Contents c = new Contents();
    Destination d = new Destination(dest);
    System.out.println(d.readLabel());
  }
  public static void main(String[] args) {
    Parcel1 p = new Parcel1();
    p.ship("Tanzania");
  }
} ///:~

Les classes internes, quand elles sont utilisées dans ship(), ressemblent à n'importe quelle autre classe. La seule différence en est que les noms sont imbriqués dans Parcel1. Mais nous allons voir dans un moment que ce n'est pas la seule différence.

Plus généralement, une classe externe peut définir une méthode qui renvoie une référence à une classe interne, comme ceci :

 
Sélectionnez
//: c08:Parcel2.java
// Renvoyer une référence à une classe interne.

public class Parcel2 {
  class Contents {
    private int i = 11;
    public int value() { return i; }
  }
  class Destination {
    private String label;
    Destination(String whereTo) {
      label = whereTo;
    }
    String readLabel() { return label; }
  }
  public Destination to(String s) {
    return new Destination(s);
  }
  public Contents cont() {
    return new Contents();
  }
  public void ship(String dest) {
    Contents c = cont();
    Destination d = to(dest);
    System.out.println(d.readLabel());
  }
  public static void main(String[] args) {
    Parcel2 p = new Parcel2();
    p.ship("Tanzania");
    Parcel2 q = new Parcel2();
    // Définition de références sur des classes internes :
    Parcel2.Contents c = q.cont();
    Parcel2.Destination d = q.to("Borneo");
  }
} ///:~

Si on veut créer un objet de la classe interne ailleurs que dans une méthode non-static de la classe externe, il faut spécifier le type de cet objet comme NomDeClasseExterne.NomDeClasseInterne, comme on peut le voir dans main().

VIII-B-1. Classes internes et transtypage ascendant

Jusqu'à présent, les classes internes ne semblent pas si dramatiques. Après tout, si le but recherché est le camouflage, Java propose déjà un très bon mécanisme pour cela - il suffit de donner l'accès package à la classe (visible seulement depuis un package) plutôt que de la déclarer comme une classe interne.

Cependant, les classes internes prennent de l'intérêt lorsque l'on commence à transtyper vers une classe de base, et en particulier vers une interface. (produire une référence vers une interface depuis un objet l'implémentant revient à transtyper vers une classe de base). C'est parce que la classe interne - l'implémentation de l'interface - peut être complètement masquée et indisponible pour tout le monde, ce qui est pratique pour cacher l'implémentation. La seule chose qu'on récupère est une référence sur la classe de base ou l'interface.

Tout d'abord, les interfaces sont définies dans leurs propres fichiers afin de pouvoir être utilisées dans tous les exemples :

 
Sélectionnez
//: c08:Destination.java
public interface Destination {
    String readLabel();
} ///:~
 
Sélectionnez
//: c08:Contents.java
public interface Contents {
    int value();
} ///:~

Maintenant Contents et Destination sont des interfaces disponibles pour le programmeur client. (L'interface, souvenez-vous, déclare automatiquement tous ses membres public).

Quand on récupère une référence sur la classe de base ou l'interface, il est possible qu'on ne puisse même pas en découvrir le type exact, comme on peut le voir dans le code suivant :

 
Sélectionnez
//: c08:TestParcel.java
// Renvoyer une référence sur une classe interne.

class Parcel3 {
    private class PContents implements Contents {
        private int i = 11;
        public int value() { return i; }
    }
    protected class PDestination implements Destination {
        private String label;
        private PDestination(String whereTo) {
            label = whereTo;
        }
        public String readLabel() { return label; }
    }
    public Destination dest(String s) {
        return new PDestination(s);
    }
    public Contents cont() {
        return new PContents();
    }
}

public class TestParcel {
    public static void main(String[] args) {
        Parcel3 p = new Parcel3();
        Contents c = p.cont();
        Destination d = p.dest("Tanzania");
        // Illégal -- ne peut accéder à une classe private :
        //! Parcel3.PContents pc = p.new PContents();
    }
} ///:~

Dans cet exemple, main( ) doit être dans une classe séparée afin de démontrer le caractère private de la classe interne PContents.

Dans Parcel3, de nouvelles particularités ont été ajoutées : la classe interne PContents est private, afin que seule Parcel3 puisse y accéder. PDestination est protected, afin que seules Parcel3, les classes du même package (puisque protected fournit aussi un accès package), et les héritiers de Parcel3 puissent accéder à PDestination. Cela signifie que le programmeur client n'a qu'une connaissance et des accès restreints à ces membres. En fait, on ne peut même pas transtyper vers une classe interne private (ou une classe interne protected à moins d'en hériter), parce qu'on ne peut accéder à son nom, comme on peut le voir dans la classe TestParcel. La classe interne private fournit donc un moyen pour le concepteur de la classe d'interdire tout code testant le type et de cacher complètement les détails de l'implémentation. De plus, l'extension d'une interface est inutile du point de vue du programmeur client puisqu'il ne peut accéder à aucune méthode additionnelle ne faisant pas partie de l'interface public. Cela permet aussi au compilateur Java de générer du code plus efficace.

Les classes normales (non internes) ne peuvent pas être déclarées private ou protected; mais uniquement public ou en accès package.

VIII-B-2. Classes internes dans les méthodes et les domaines d'application

Ce qu'on a pu voir jusqu'à présent constitue l'utilisation typique des classes internes. En général, le code impliquant des classes internes que vous serez amené à lire et à écrire ne mettra en œuvre que des classes internes « régulières », et sera simple à comprendre. Cependant, le support des classes internes est relativement complet, et il existe de nombreuses autres manières, plus obscures, de les utiliser si on le souhaite ; les classes internes peuvent être créées à l'intérieur d'une méthode ou même d'une portée quelconque. Deux raisons possibles à cela :

  • Comme montré précédemment, on implémente une interface d'un certain type afin de pouvoir créer et renvoyer une référence.
  • On résout un problème compliqué pour lequel la création d'une classe aiderait grandement, mais on ne veut pas la rendre publiquement accessible.

Dans les exemples suivants, le code précédent est modifié afin d'utiliser :

  • Une classe définie dans une méthode
  • Une classe définie dans une portée à l'intérieur d'une méthode
  • Une classe anonyme implémentant une interface
  • Une classe anonyme étendant une classe qui dispose d'un constructeur autre que le constructeur par défaut
  • Une classe anonyme réalisant des initialisations de champs
  • Une classe anonyme qui se construit en initialisant des instances (les classes internes anonymes ne peuvent pas avoir de constructeurs)

Bien que ce soit une classe ordinaire avec une implémentation, Wrapping est aussi utilisée comme une « interface » commune pour ses classes dérivées :

 
Sélectionnez
//: c08:Wrapping.java
public class Wrapping {
    private int i;
    public Wrapping(int x) { i = x; }
    public int value() { return i; }
} ///:~

Notez que Wrapping dispose d'un constructeur requérant un argument, afin de rendre les choses un peu plus intéressantes.

Le premier exemple montre la création d'une classe entière dans l'étendue d'une méthode (au lieu de l'étendue d'une autre classe). Cela est appelé une classe interne locale :

 
Sélectionnez
//: c08:Parcel4.java
//Imbrication une classe au sein d'une méthode.

public class Parcel4 {
    public Destination dest(String s) {
        class PDestination implements Destination {
            private String label;
            private PDestination(String whereTo) {
                label = whereTo;
            }
            public String readLabel() { return label; }
        }
        return new PDestination(s);
    }
    public static void main(String[] args) {
        Parcel4 p = new Parcel4();
        Destination d = p.dest("Tanzania");
    }
} ///:~

La classe PDestination est une partie de dest( ) plutôt que de Parcel4. (Notez aussi qu'on peut utiliser l'identifiant de classe PDestination pour une classe interne à l'intérieur de chaque classe du même sous-répertoire sans collision de nom). Par conséquent, PDestination ne peut pas être accédée en dehors de dest( ). Notez le transtypage ascendant réalisé par l'instruction de retour - dest( ) ne peut renvoyer qu'une référence à Destination, la classe de base. Bien sûr, le fait que le nom de la classe PDestination soit placé à l'intérieur de dest( ) ne veut pas dire que PDestination n'est pas un objet valide une fois revenu de dest( ).

L'exemple suivant montre comment on peut imbriquer une classe interne à l'intérieur de n'importe quelle étendue arbitraire :

 
Sélectionnez
//: c08:Parcel5.java
// Imbrication une classe à l'intérieur d'une étendue

public class Parcel5 {
    private void internalTracking(boolean b) {
        if(b) {
            class TrackingSlip {
                private String id;
                TrackingSlip(String s) {
                    id = s;
                }
                String getSlip() { return id; }
            }
            TrackingSlip ts = new TrackingSlip("slip");
            String s = ts.getSlip();
        }
        // Utilisation impossible ici ! En dehors de l'étendue :
        //! TrackingSlip ts = new TrackingSlip("x");
    }
    public void track() { internalTracking(true); }
    public static void main(String[] args) {
        Parcel5 p = new Parcel5();
        p.track();
    }
} ///:~

La classe TrackingSlip est définie dans l'étendue de l'instruction if. Cela ne veut pas dire que la classe classe est créée conditionnellement - elle est compilée avec tout le reste. Cependant, elle n'est pas accessible en dehors de l'étendue dans laquelle elle est définie. Mis à part cette restriction, elle ressemble à n'importe quelle autre classe ordinaire.

VIII-B-3. Anonymous inner classes

The next example looks a little strange:

 
Sélectionnez
//: c08:Parcel6.java
// A method that returns an anonymous inner class.

public class Parcel6 {
    public Contents cont() {
        return new Contents() {
            private int i = 11;
            public int value() { return i; }
        }; // Semicolon required in this case
    }
    public static void main(String[] args) {
        Parcel6 p = new Parcel6();
        Contents c = p.cont();
    }
} ///:~

The cont( ) method combines the creation of the return value with the definition of the class that represents that return value! In addition, the class is anonymous; it has no name. To make matters a bit worse, it looks like you're starting out to create a Contents object:

 
Sélectionnez
return new Contents()

But then, before you get to the semicolon, you say, « But wait, I think I'll slip in a class definition »:

 
Sélectionnez
return new Contents() {
    private int i = 11;
    public int value() { return i; }
};

What this strange syntax means is: « Create an object of an anonymous class that's inherited from Contents. » The reference returned by the new expression is automatically upcast to a Contents reference. The anonymous inner-class syntax is a shorthand for:

 
Sélectionnez
class MyContents implements Contents {
    private int i = 11;
    public int value() { return i; }
}
return new MyContents();

In the anonymous inner class, Contents is created by using a default constructor. The following code shows what to do if your base class needs a constructor with an argument:

 
Sélectionnez
//: c08:Parcel7.java
// An anonymous inner class that calls
// the base-class constructor.

public class Parcel7 {
    public Wrapping wrap(int x) {
        // Base constructor call:
        return new Wrapping(x) { // Pass constructor argument.
            public int value() {
                return super.value() * 47;
            }
        }; // Semicolon required
    }
    public static void main(String[] args) {
        Parcel7 p = new Parcel7();
        Wrapping w = p.wrap(10);
    }
} ///:~

That is, you simply pass the appropriate argument to the base-class constructor, seen here as the x passed in new Wrapping(x).

The semicolon at the end of the anonymous inner class doesn't mark the end of the class body (as it does in C++). Instead, it marks the end of the expression that happens to contain the anonymous class. Thus, it's identical to the use of the semicolon everywhere else.

You can also perform initialization when you define fields in an anonymous class:

 
Sélectionnez
//: c08:Parcel8.java
// An anonymous inner class that performs
// initialization. À briefer version of Parcel4.java.

public class Parcel8 {
    // Argument must be final to use inside
    // anonymous inner class:
    public Destination dest(final String dest) {
        return new Destination() {
            private String label = dest;
            public String readLabel() { return label; }
        };
    }
    public static void main(String[] args) {
        Parcel8 p = new Parcel8();
        Destination d = p.dest("Tanzania");
    }
} ///:~

If you're defining an anonymous inner class and want to use an object that's defined outside the anonymous inner class, the compiler requires that the argument reference be final, like the argument to dest( ).If you forget, you'll get a compile-time error message.

As long as you're simply assigning a field, the approach in this example is fine. But what if you need to perform some constructor-like activity? You can't have a named constructor in an anonymous class (since there's no name!), but with instance initialization, you can, in effect, create a constructor for an anonymous inner class, like this:

 
Sélectionnez
//: c08:AnonymousConstructor.java
// Creating a constructor for an anonymous inner class.
import com.bruceeckel.simpletest.*;

abstract class Base {
    public Base(int i) {
        System.out.println("Base constructor, i = " + i);
    }
    public abstract void f();
}

public class AnonymousConstructor {
    private static Test monitor = new Test();
    public static Base getBase(int i) {
        return new Base(i) {
            {
                System.out.println("Inside instance initializer");
            }
            public void f() {
                System.out.println("In anonymous f()");
            }
        };
    }
    public static void main(String[] args) {
        Base base = getBase(47);
        base.f();
        monitor.expect(new String[] {
            "Base constructor, i = 47",
            "Inside instance initializer",
            "In anonymous f()"
        });
    }
} ///:~

In this case, the variable i did not have to be final. While i is passed to the base constructor of the anonymous class, it is never directly used inside the anonymous class.

Here's the « parcel » theme with instance initialization. Note that the arguments to dest( ) must be final since they are used within the anonymous class:

 
Sélectionnez
//: c08:Parcel9.java
// Using "instance initialization" to perform
// construction on an anonymous inner class.
import com.bruceeckel.simpletest.*;

public class Parcel9 {
    private static Test monitor = new Test();
    public Destination
    dest(final String dest, final float price) {
        return new Destination() {
            private int cost;
            // Instance initialization for each object:
            {
                cost = Math.round(price);
                if(cost > 100)
                    System.out.println("Over budget!");
            }
            private String label = dest;
            public String readLabel() { return label; }
        };
    }
    public static void main(String[] args) {
        Parcel9 p = new Parcel9();
        Destination d = p.dest("Tanzania", 101.395F);
        monitor.expect(new String[] {
            "Over budget!"
        });
    }
} ///:~

Inside the instance initializer you can see code that couldn't be executed as part of a field initializer (that is, the if statement). So in effect, an instance initializer is the constructor for an anonymous inner class. Of course, it's limited; you can't overload instance initializers, so you can have only one of these constructors.

VIII-B-4. Le lien vers la classe externe

Jusqu'à présent, les classes internes apparaissent juste comme un mécanisme de camouflage de nom et d'organisation du code, ce qui est intéressant, mais pas vraiment indispensable. Cependant, ici, les choses prennent une tournure inattendue. Quand on crée une classe interne, un objet de cette classe interne possède un lien vers l'objet extérieur qui l'a créé, il peut donc accéder aux membres de cet objet externe -sans aucune qualification spéciale. De plus, les classes internes ont accès à tous les éléments de la classe externe. (35) L'exemple suivant le démontre :

 
Sélectionnez
//: c08:Sequence.java
// Contient une séquence d'Objects.
import com.bruceeckel.simpletest.*;

interface Selector {
    boolean end();
    Object current();
    void next();
}

public class Sequence {
    private static Test monitor = new Test();
    private Object[] objects;
    private int next = 0;
    public Sequence(int size) { objects = new Object[size]; }
    public void add(Object x) {
        if(next < objects.length)
            objects[next++] = x;
    }
    private class SSelector implements Selector {
        private int i = 0;
        public boolean end() { return i == objects.length; }
        public Object current() { return objects[i]; }
        public void next() { if(i < objects.length) i++; }
    }
    public Selector getSelector() { return new SSelector(); }
    public static void main(String[] args) {
        Sequence sequence = new Sequence(10);
        for(int i = 0; i < 10; i++)
            sequence.add(Integer.toString(i));
        Selector selector = sequence.getSelector();
        while(!selector.end()) {
            System.out.println(selector.current());
            selector.next();
        }
        monitor.expect(new String[] {
            "0",
            "1",
            "2",
            "3",
            "4",
            "5",
            "6",
            "7",
            "8",
            "9"
        });
    }
} ///:~

La Sequence est simplement un tableau d'Object de taille fixe enveloppé dans une classe. On peut appeler add( ) pour ajouter un nouvel Object à la fin de la séquence (s'il reste de la place). Pour retrouver chacun des objets dans une Sequence, il existe une interface appelée Selector, qui permet de vérifier si l'on se trouve à la fin end( ), de récupérer l'Object current( ), et de se déplacer vers l'Object next( ) dans la Sequence. Comme Selector est une interface, beaucoup d'autres classes peuvent implémenter l'interface à leurs façons, et de nombreuses méthodes peuvent prendre interface comme un argument, afin de créer du code générique.

Ici, SSelector est une classe private qui fournit les fonctionnalités de Selector. Dans main( ), on peut voir la création d'une Sequence, suivie par l'addition d'un certain nombre d'objets String. Un Selector est alors produit grâce à un appel à getSelector( ), et celui-ci est alors utilisé pour se déplacer dans la Sequence et sélectionner chaque item.

Au premier abord, la création d'un SSelector ressemble à n'importe quelle autre classe interne. Mais regardez-la attentivement. Notez que chacune des méthodes -end( ), current( ), et next( )- utilisent objects, qui est une référence n'appartenant pas à SSelector, mais à la place un champ private de la classe externe. Cependant, la classe interne peut accéder aux méthodes et aux champs de la classe externe comme si elle les possédait. Ceci est très pratique, comme on peut le voir dans cet exemple.

Une classe interne a donc automatiquement accès aux membres de la classe externe. Comment cela est-il possible ? La classe interne doit garder une référence de l'objet de la classe externe responsable de sa création. Ainsi, quand on accède à un membre de la classe externe, cette référence (cachée) est utilisée pour sélectionner ce membre. Heureusement, le compilateur gère tous ces détails pour nous, mais vous pouvez maintenant comprendre qu'un objet d'une classe interne ne peut être créé qu'en association avec un objet de la classe externe. La construction d'un objet d'une classe interne requiert une référence sur l'objet de la classe externe, et le compilateur se plaindra s'il ne peut accéder à cette référence. La plupart du temps cela se fait sans aucune intervention de la part du programmeur.

VIII-B-5. Nested classes

If you don't need a connection between the inner class object and the outer class object, then you can make the inner class static. This is commonly called a nested class. (36) To understand the meaning of static when applied to inner classes, you must remember that the object of an ordinary inner class implicitly keeps a reference to the object of the enclosing class that created it. This is not true, however, when you say an inner class is static. À nested class means:

  • You don't need an outer-class object in order to create an object of a nested class.
  • You can't access a non-static outer-class object from an object of a nested class.

Nested classes are different from ordinary inner classes in another way, as well. Fields and methods in ordinary inner classes can only be at the outer level of a class, so ordinary inner classes cannot have static data, static fields, or nested classes. However, nested classes can have all of these:

 
Sélectionnez
//: c08:Parcel10.java
// Nested classes (static inner classes).

public class Parcel10 {
    private static class ParcelContents implements Contents {
        private int i = 11;
        public int value() { return i; }
    }
    protected static class ParcelDestination
    implements Destination {
        private String label;
        private ParcelDestination(String whereTo) {
            label = whereTo;
        }
        public String readLabel() { return label; }
        // Nested classes can contain other static elements:
        public static void f() {}
        static int x = 10;
        static class AnotherLevel {
            public static void f() {}
            static int x = 10;
        }
    }
    public static Destination dest(String s) {
        return new ParcelDestination(s);
    }
    public static Contents cont() {
        return new ParcelContents();
    }
    public static void main(String[] args) {
        Contents c = cont();
        Destination d = dest("Tanzania");
    }
} ///:~

In main( ), no object of Parcel10 is necessary; instead, you use the normal syntax for selecting a static member to call the methods that return references to Contents and Destination.

As you will see shortly, in an ordinary (non-static) inner class, the link to the outer class object is achieved with a special this reference. A nested class does not have this special this reference, which makes it analogous to a static method.

Normally, you can't put any code inside an interface, but a nested class can be part of an interface. Since the class is static,it doesn't violate the rules for interfaces-the nested class is only placed inside the namespace of the interface:

 
Sélectionnez
//: c08:IInterface.java
// Nested classes inside interfaces.

public interface IInterface {
    static class Inner {
        int i, j, k;
        public Inner() {}
        void f() {}
    }
} ///:~

Earlier in this book I suggested putting a main( ) in every class to act as a test bed for that class. One drawback to this is the amount of extra compiled code you must carry around. If this is a problem, you can use a nested class to hold your test code:

 
Sélectionnez
//: c08:TestBed.java
// Putting test code in a nested class.

public class TestBed {
    public TestBed() {}
    public void f() { System.out.println("f()"); }
    public static class Tester {
        public static void main(String[] args) {
            TestBed t = new TestBed();
            t.f();
        }
    }
} ///:~

This generates a separate class called TestBed$Tester (to run the program, you say java TestBed$Tester). You can use this class for testing, but you don't need to include it in your shipping product; you can simply delete TestBed$Tester.class before packaging things up.

VIII-B-6. Se référer à l'objet de la classe externe

Si on a besoin de produire la référence à l'objet de la classe externe, il faut utiliser le nom de la classe externe suivi par un point et this. Par exemple, dans la classe Sequence.SSelector, chacune des méthodes peut accéder à la référence à la classe externe Sequence stockée en utilisant Sequence.this. Le type de la référence obtenue est automatiquement correct (il est connu et vérifié lors de la compilation, il n'y a donc aucune pénalité sur les performances lors de l'exécution).

Parfois on veut demander à un autre objet de créer un objet de l'une de ses classes internes. Pour faire cela, il faut fournir une référence à l'autre objet de la classe externe dans l'expression new, comme ceci :

 
Sélectionnez
//: c08:Parcel11.java
// Création d'instances de classes internes.

public class Parcel11 {
    class Contents {
        private int i = 11;
        public int value() { return i; }
    }
    class Destination {
        private String label;
        Destination(String whereTo) { label = whereTo; }
        String readLabel() { return label; }
    }
    public static void main(String[] args) {
        Parcel11 p = new Parcel11();
        // On doit utiliser une instance de la classe externe
        // pour créer une instance de la classe interne : 
        Parcel11.Contents c = p.new Contents();
        Parcel11.Destination d = p.new Destination("Tanzania");
    }
} ///:~

Pour créer un objet de la classe interne directement, il ne faut pas utiliser la même syntaxe et se référer au nom de la classe externe Parcel11 comme on pourrait s'y attendre ; mais à la place il faut utiliser un objet de la classe externe pour créer un objet de la classe interne :

 
Sélectionnez
Parcel11.Contents c = p.new Contents();

Il n'est donc pas possible de créer un objet de la classe interne sans disposer déjà d'un objet de la classe externe. Ceci parce qu'un objet de la classe interne est connecté silencieusement avec l'objet de la classe externe qui l'a créé. Cependant, si la classe est imbriquée (une classe interne static), alors elle n'a pas besoin d'une référence sur un objet de la classe externe.

VIII-B-7. Classe interne à plusieurs niveaux d'imbrication

(37) Une classe interne peut se situer à n'importe quel niveau d'imbrication - elle pourra toujours accéder de manière transparente à tous les membres de toutes les classes l'entourant, comme on peut le voir ici :

 
Sélectionnez
//: c08:MultiNestingAccess.java
// Les classes imbriquées peuvent accéder à tous les membres de tous
// les niveaux des classes dans lesquelles elles sont imbriquées.

class MNA {
    private void f() {}
    class A {
        private void g() {}
        public class B {
            void h() {
                g();
                f();
            }
        }
    }
}

public class MultiNestingAccess {
    public static void main(String[] args) {
        MNA mna = new MNA();
        MNA.À mnaa = mna.new A();
        MNA.A.B mnaab = mnaa.new B();
        mnaab.h();
    }
} ///:~

On peut voir que dans MNA.A.B, les méthodes g( ) et f( ) sont appelées sans aucune qualification (malgré le fait qu'elles soient private). Cet exemple présente aussi la syntaxe nécessaire pour créer des objets de classes internes imbriquées quand on crée ces objets depuis une autre classe. La syntaxe « .new » fournit la portée correcte, donc on n'a pas besoin de qualifier le nom de la classe dans l'appel du constructeur.

VIII-B-8. Hériter d'une classe interne

Comme le constructeur d'une classe interne doit s'attacher à une référence à l'objet de la classe externe, les choses sont un peu plus compliquées lorsqu'on dérive une classe interne. Le problème est que la référence « secrète » sur l'objet de la classe externe doit être initialisée, et dans la classe dérivée il n'y a plus d'objet par défaut auquel se rattacher. La solution est donc d'utiliser une syntaxe qui rende cette association explicite :

 
Sélectionnez
//: c08:InheritInner.java
// Hériter d'une classe interne.

class WithInner {
    class Inner {}
}

public class InheritInner extends WithInner.Inner {
    //! InheritInner() {} // Ne compilera pas.
    InheritInner(WithInner wi) {
        wi.super();
    }
    public static void main(String[] args) {
        WithInner wi = new WithInner();
        InheritInner ii = new InheritInner(wi);
    }
} ///:~

On peut voir que InheritInner étend juste la classe interne, et non la classe externe. Mais quand vient le temps de créer un constructeur, celui fourni par défaut ne convient pas, et on ne peut se contenter de passer une référence à un objet externe. De plus, on doit utiliser la syntaxe

 
Sélectionnez
enclosingClassReference.super();

à l'intérieur du constructeur. Ceci fournit la référence nécessaire et le programme pourra alors être compilé.

VIII-B-9. Les classes internes peuvent-elles redéfinies ?

Que se passe-t-il quand on crée une classe interne, qu'on hérite la classe externe et qu'on redéfinit la classe interne ? Autrement dit, est-il possible de redéfinir entièrement la classe interne ? Ce concept semblerait particulièrement puissant, mais « redéfinir » une classe interne comme si c'était une autre méthode de la classe externe ne fait en réalité rien du tout :

 
Sélectionnez
//: c08:BigEgg.java
// Une classe interne ne peut être redéfinie comme une méthode. 
import com.bruceeckel.simpletest.*;

class Egg {
    private Yolk y;
    protected class Yolk {
        public Yolk() { System.out.println("Egg.Yolk()"); }
    }
    public Egg() {
        System.out.println("New Egg()");
        y = new Yolk();
    }
}

public class BigEgg extends Egg {
    private static Test monitor = new Test();
    public class Yolk {
        public Yolk() { System.out.println("BigEgg.Yolk()"); }
    }
    public static void main(String[] args) {
        new BigEgg();
        monitor.expect(new String[] {
            "New Egg()",
            "Egg.Yolk()"
        });
    }
} ///:~

Le constructeur par défaut est généré automatiquement par le compilateur, et il appelle le constructeur par défaut de la classe de base. On pourrait penser que puisqu'on crée un BigEgg, a version « redéfinie » de Yolk serait utilisée, mais ce n'est pas le cas, comme on peut le voir d'après la sortie.

Cet exemple montre qu'il n'y a aucune magie spéciale associée aux classes internes quand on hérite d'une classe externe. Les deux classes internes sont des entités complètement séparées, chacune dans leur propre espace de noms. Cependant, il est toujours possible d'hériter explicitement la classe interne :

 
Sélectionnez
//: c08:BigEgg2.java
// Héritage propre d'une classe interne.
import com.bruceeckel.simpletest.*;

class Egg2 {
    protected class Yolk {
        public Yolk() { System.out.println("Egg2.Yolk()"); }
        public void f() { System.out.println("Egg2.Yolk.f()");}
    }
    private Yolk y = new Yolk();
    public Egg2() { System.out.println("New Egg2()"); }
    public void insertYolk(Yolk yy) { y = yy; }
    public void g() { y.f(); }
}

public class BigEgg2 extends Egg2 {
    private static Test monitor = new Test();
    public class Yolk extends Egg2.Yolk {
        public Yolk() { System.out.println("BigEgg2.Yolk()"); }
        public void f() {
            System.out.println("BigEgg2.Yolk.f()");
        }
    }
    public BigEgg2() { insertYolk(new Yolk()); }
    public static void main(String[] args) {
        Egg2 e2 = new BigEgg2();
        e2.g();
        monitor.expect(new String[] {
            "Egg2.Yolk()",
            "New Egg2()",
            "Egg2.Yolk()",
            "BigEgg2.Yolk()",
            "BigEgg2.Yolk.f()"
        });
    }
} ///:~

Maintenant BigEgg2.Yolk étend explicitement Egg2.Yolk et redéfinit ses méthodes. La méthode insertYolk( ) permet à BigEgg2 de faire un transtypage ascendant sur un de ses propres objets Yolk dans la référence y de Egg2, donc quand g( ) appelle y.f( ), la version redéfinie de f( ) est utilisée. Le second appel à Egg2.Yolk( ) est l'appel du constructeur de la classe de base depuis le constructeur de BigEgg2.Yolk. On peut voir que la version redéfinie de f( ) est utilisée lorsque g( ) est appelée.

VIII-B-10. Les classes internes locales

Comme indiqué dans les paragraphes précédents, les classes internes peuvent être créées dans un bloc de code, typiquement elles sont créées dans le corps d'une méthode. Une classe interne locale ne peut pas avoir de spécificateur d'accès, car elle ne fait pas partie de la classe externe, mais elle a accès aux variables finales dans le bloc de code courant ainsi qu'à tous les membres de la classe englobante. Voici un exemple comparant la création d'une classe interne locale avec une classe interne anonyme :

 
Sélectionnez
//: c08:LocalInnerClass.java
// Garde une séquence d'Objects.
import com.bruceeckel.simpletest.*;

interface Counter {
    int next();
}

public class LocalInnerClass {
    private static Test monitor = new Test();
    private int count = 0;
    Counter getCounter(final String name) {
        // Une classe interne locale :
        class LocalCounter implements Counter {
            public LocalCounter() {
                // Les classes interne locale peuvent avoir un constructeur
                System.out.println("LocalCounter()");
            }
            public int next() {
                System.out.print(name); // Accède à la variable finale
                return count++;
            }
        }
        return new LocalCounter();
    }
    // La même chose, mais avec une classe interne anonyme :
    Counter getCounter2(final String name) {
        return new Counter() {
            // Les classes interne anonymes ne peuvent pas
            // avoir de constructeur, mais seulement
            // une initialisation d'instance :
            {
                System.out.println("Counter()");
            }
            public int next() {
                System.out.print(name); // Accède à la variable finale
                return count++;
            }
        };
    }
    public static void main(String[] args) {
        LocalInnerClass lic = new LocalInnerClass();
        Counter
            c1 = lic.getCounter("Local inner "),
            c2 = lic.getCounter2("Anonymous inner ");
        for(int i = 0; i < 5; i++)
            System.out.println(c1.next());
        for(int i = 0; i < 5; i++)
            System.out.println(c2.next());
        monitor.expect(new String[] {
            "LocalCounter()",
            "Counter()",
            "Local inner 0",
            "Local inner 1",
            "Local inner 2",
            "Local inner 3",
            "Local inner 4",
            "Anonymous inner 5",
            "Anonymous inner 6",
            "Anonymous inner 7",
            "Anonymous inner 8",
            "Anonymous inner 9"
        });
    }
} ///:~

Counter retourne la valeur suivante dans la séquence. Cette classe est implémenté à la fois comme une classe interne anonyme et comme une classe interne locale, les deux implémentations ayant les mêmes comportements et les mêmes capacités. Comme le nom de la classe interne locale n'est pas accessible en dehors de la méthode, la seule justification de l'utilisation d'une classe interne locale au lieu d'une classe anonyme est le besoin d'un constructeur nommé et/ou d'un constructeur surchargé, car une classe interne anonyme ne peut utiliser que l'initialisation d'instance.

L'unique raison pour laquelle on peut préférer une classe interne à une classe interne anonyme est si l'on a besoin de créer plus d'une instance de cette classe.

VIII-B-11. Identifiants des classes internes

Puisque chaque classe produit un fichier .class qui contient toutes les informations concernant la création d'objets de ce type (ces informations produisent une « métaclasse » appelée l'objet Class), on peut deviner que les classes internes produisent aussi des fichiers .class qui contiennent des informations pour leurs objets Class. La nomenclature de ces fichiers / classes est stricte : le nom de la classe externe, suivie par un '$', suivi du nom de la classe interne. Par exemple, les fichiers .class créés par LocalInnerClass.java incluent :

 
Sélectionnez
Counter.class
LocalInnerClass$2.class
LocalInnerClass$1LocalCounter.class
LocalInnerClass.class

Si les classes internes sont anonymes, le compilateur génère simplement des nombres comme identifiants de classe interne. Si des classes internes sont imbriquées dans d'autres classes internes, leur nom est simplement ajouté après un '$' et le nom des identifiants des classes externes.

Bien que cette gestion interne des noms soit simple et directe, elle est aussi robuste et gère la plupart des situations. (38) Et comme cette notation est la notation standard pour Java, les fichiers générés sont automatiquement indépendants de la plateforme (Notez que le compilateur Java modifie les classes internes d'un tas d'autres manières afin de les faire fonctionner).

VIII-C. Raison d'être des classes internes

Jusqu'à présent, on a vu beaucoup de syntaxes et la sémantique décrivant la façon dont les classes internes fonctionnent, mais cela ne répond pas à la question du pourquoi de leur existence. Pourquoi Sun s'est-il donné tant de mal pour ajouter au langage cette fonctionnalité fondamentale ?

Typiquement, la classe interne hérite d'une classe ou implémente une interface, et le code de la classe interne manipule l'objet de la classe externe l'ayant créée. On peut donc dire qu'une classe interne est une sorte de fenêtre dans la classe externe.

Mais si j'ai juste besoin d'une référence sur une interface, pourquoi ne pas implémenter cette interface directement dans la classe externe ? La réponse à cette question allant au cœur des classes internes est : « Si c'est tout ce dont vous avez besoin, alors c'est ainsi qu'il faut procéder ». Alors qu'est-ce qui distingue une classe interne implémentant une interface d'une classe externe implémentant cette même interface ? C'est tout simplement qu'on ne dispose pas toujours des facilités fournies par les interfaces - quelquefois on est obligé de travailler avec des implémentations. Voici donc la raison principale d'utiliser des classes internes :

Chaque classe interne peut hériter indépendamment d'une implémentation. La classe interne n'est pas limitée par le fait que la classe externe hérite déjà d'une implémentation.

Sans cette capacité que fournissent les classes internes d'hériter - dans la pratique - de plus d'une classe concrète ou abstract, certains problèmes de conception ou de programmation seraient impossibles à résoudre. Les classes internes peuvent donc être considérées comme la suite de la solution au problème de l'héritage multiple. Les interfaces résolvent une partie du problème, mais les classes internes permettent réellement « l'héritage multiple d'implémentations ». Les classes internes vous permettent effectivement de dériver plus d'une non-interfaces.

Pour voir ceci plus en détail, imaginons une situation dans laquelle une classe doit implémenter d'une manière ou d'une autre deux interfaces. Du fait de la flexibilité des interfaces, on a deux choix : une unique classe ou une classe interne :

 
Sélectionnez
//: c08:MultiInterfaces.java
// Deux façons pour une classe d'implémenter des interfaces multiples.

interface A {}
interface B {}

class X implements A, B {}

class Y implements A {
    B makeB() {
        // Classe interne anonyme :
        return new B() {};
    }
}

public class MultiInterfaces {
    static void takesA(A a) {}
    static void takesB(B b) {}
    public static void main(String[] args) {
        X x = new X();
        Y y = new Y();
        takesA(x);
        takesA(y);
        takesB(x);
        takesB(y.makeB());
    }
} ///:~

Bien sûr, ceci suppose que la structure du code rend logique l'une ou l'autre de ces solutions. La nature du problème vous fournira généralement des indices pour choisir entre une classe unique ou une classe interne. Mais en l'absence de toute autre contrainte, l'approche choisie dans l'exemple précédent ne fait pas vraiment de différence du point de vue de l'implémentation. Les deux fonctionnent.

Cependant, si on a des classes abstract ou concrètes à la place des interfaces, vous êtes subitement obligés de recourir aux classes internes si la classe doit implémenter chacune :

 
Sélectionnez
//: c08:MultiImplementation.java
// Avec des classes concrètes ou abstract, les classes
// internes constituent le seul moyen de mettre en oeuvre
// « l'héritage multiple d'implémentations ».
package c08;

class D {}
abstract class E {}

class Z extends D {
    E makeE() { return new E() {}; }
}

public class MultiImplementation {
    static void takesD(D d) {}
    static void takesE(E e) {}
    public static void main(String[] args) {
        Z z = new Z();
        takesD(z);
        takesE(z.makeE());
    }
} ///:~

Si vous n'aviez pas à résoudre le problème de « l'héritage multiple d'implémentations », vous pourriez tout à fait coder tout le reste sans avoir besoin des classes internes. Mais les classes internes fournissent ces fonctionnalités supplémentaires :

  • Les classes internes peuvent avoir plusieurs instances, chacune avec ses propres informations d'état indépendantes des informations de l'objet de la classe externe.
  • Dans une seule 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.
  • 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.
  • Il n'y a pas de relation « est-un », potentiellement ambiguë, 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. On pourrait avoir facilement une seconde méthode, getRSelector( ), qui produirait un Selector parcourant la séquence dans l'ordre inverse. Cette flexibilité n'est possible qu'avec les classes internes.

VIII-C-1. Fermetures & callbacks

Une fermeture est un objet qui retient des informations de la portée dans laquelle il a été créé. À 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 le livre. 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 :

 
Sélectionnez
//: c08:Callbacks.java
        // Utilisation des classes internes pour les callbacks
        import com.bruceeckel.simpletest.*;

        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 {
            void increment() {
            System.out.println("Other operation");
            }
            static void f(MyIncrement mi) { mi.increment(); }
        }

        // Si la classe doit 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 {
            private static Test monitor = new Test();
            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();
            monitor.expect(new String[] {
                "Other operation",
                "1",
                "2",
                "1",
                "2"
            });
            }
        } ///:~

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() et rien d'autre (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 14, où ils sont utilisés immodérément pour implémenter les interfaces graphiques utilisateurs (GUI).

VIII-C-2. Inner classes & control frameworks

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 typiquement de dériver d'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é (c'est un exemple du design pattern Template Method ; voir Thinking in Patterns (avec Java) sur www.BruceEckel.com). 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 14, 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 œuvre 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. Cette information est fournie au cours de l'héritage, lorsque le « modèle de la méthode » est implémenté.

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 :

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

    public abstract class Event {
        private long eventTime;
        protected final long delayTime;
        public Event(long delayTime) {
        this.delayTime = delayTime;
        start();
        }
        public void start() { // Permet de redémarrer
        eventTime = System.currentTimeMillis() + delayTime;
        }
        public boolean ready() {
        return System.currentTimeMillis() >= eventTime;
        }
        public abstract void action();
    } ///:~

Le constructeur stocke l'heure (à partir du moment de la création de l'objet) à laquelle on veut que l'Event soit exécuté, afin de lancer par la suite des appels start( ), qui prennent le temps courant et ajoutent l'effet de latence inhérent au temps d'exécution de l'événement. Plutôt que d'être inclus dans le constructeur, start( ) est une méthode distincte. De cette façon, il vous permet de redémarrer le chronomètre après la fin de l'évènement, et ainsi, l'objet est réutilisable. Par exemple, si vous voulez un événement répétitif, vous pouvez simplement appeler start( ) dans votre méthode action( ).

ready( ) indique quand il est temps d'exécuter la méthode action( ). Évidemment, ready( ) peut-être redéfini dans une classe dérivée pour baser les Event sur autre chose que le temps.

Le fichier suivant contient la structure de contrôle proprement dite qui gère et déclenche les événements. Les objets Event sont contenus dans un conteneur d'objets de type ArrayList, dont vous en apprendrez plus dans le Chapter 11. Pour l'instant, tout ce que vous devez savoir est que add( ) permet de joindre un Object à la fin de l'ArrayList, size( ) retourne le nombre d'éléments dans l'ArrayList, get( ) permet de récupérer un élément de l'ArrayList à un index spécifique, et remove( ) supprime un élément de l'ArrayList, en lui donnant le numéro de l'élément à supprimer.

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

        public class Controller {
            // Un objet de java.util pour stocker les objects Event :
            private List eventList = new ArrayList();
            public void addEvent(Event c) { eventList.add(c); }
            public void run() {
            while(eventList.size() > 0) {
                for(int i = 0; i < eventList.size(); i++) {
                Event e = (Event)eventList.get(i);
                if(e.ready()) {
                    System.out.println(e);
                    e.action();
                    eventList.remove(i);
                }
                }
            }
            }
        } ///:~

La méthode run( ) boucle à travers un eventList, à la recherche d'un objet Event qui exécute ready( ). Pour chacun, il trouve ready( ), il affiche les informations en utilisant la méthode toString( ) de l'objet, appelle la méthode action( ), 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 :

  • 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.
  • 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. (39) 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 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 :

 
Sélectionnez
        //: 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 com.bruceeckel.simpletest.*;
        import c08.controller.*;

        public class GreenhouseControls extends Controller {
            private static Test monitor = new Test();
            private boolean light = false;
            public class LightOn extends Event {
            public LightOn(long delayTime) { super(delayTime); }
            public void action() {
                // Placer ici du code de contrôle hardware pour
                // réellement allumer la lumière.
                light = true;
            }
            public String toString() { return "Light is on"; }
            }
            public class LightOff extends Event {
            public LightOff(long delayTime) { super(delayTime); }
            public void action() {
                // Placer ici du code de contrôle hardware pour
                // réellement éteindre la lumière.
                light = false;
            }
            public String toString() { return "Light is off"; }
            }
            private boolean water = false;
            public class WaterOn extends Event {
            public WaterOn(long delayTime) { super(delayTime); }
            public void action() {
                // Placer ici du code de contrôle hardware.
                water = true;
            }
            public String toString() {
                return "Greenhouse water is on";
            }
            }
            public class WaterOff extends Event {
            public WaterOff(long delayTime) { super(delayTime); }
            public void action() {
                // Placer ici du code de contrôle hardware.
                water = false;
            }
            public String toString() {
                return "Greenhouse water is off";
            }
            }
            private String thermostat = "Day";
            public class ThermostatNight extends Event {
            public ThermostatNight(long delayTime) {
                super(delayTime);
            }
            public void action() {
                // Placer ici du code de contrôle hardware.
                thermostat = "Night";
            }
            public String toString() {
                return "Thermostat on night setting";
            }
            }
            public class ThermostatDay extends Event {
            public ThermostatDay(long delayTime) {
                super(delayTime);
            }
            public void action() {
                // Placer ici du code de contrôle hardware.
                thermostat = "Day";
            }
            public String toString() {
                return "Thermostat on day setting";
            }
            }
            // Un exemple d'une action() qui insère une nouvelle
            // instance de son type dans la liste d'Event :
            public class Bell extends Event {
            public Bell(long delayTime) { super(delayTime); }
            public void action() {
                addEvent(new Bell(delayTime));
            }
            public String toString() { return "Bing!"; }
            }
            public class Restart extends Event {
            private Event[] eventList;
            public Restart(long delayTime, Event[] eventList) {
                super(delayTime);
                this.eventList = eventList;
                for(int i = 0; i < eventList.length; i++)
                addEvent(eventList[i]);
            }
            public void action() {
                for(int i = 0; i < eventList.length; i++) {
                eventList[i].start(); // Relance chaque Event
                addEvent(eventList[i]);
                }
                start(); // Relance cet Event
                addEvent(this);
            }
            public String toString() {
                return "Restarting system";
            }
            }
            public class Terminate extends Event {
            public Terminate(long delayTime) { super(delayTime); }
            public void action() { System.exit(0); }
            public String toString() { return "Terminating";  }
            }
        } ///:~

Notez que light, water et thermostat appartiennent tous à la classe externe GreenhouseControls, et donc les classes internes peuvent accéder à ces champs sans qualification ou permission particulière. De plus, la plupart des méthodes action() effectuent un contrôle hardware.

La plupart des classes Event sont similaires, mais Bell et Restart sont spéciales. Bell sonne et elle ajoute un nouvel objet Bell à la liste des événements afin de sonner à nouveau plus tard. Notez comme les classes internes semblent bénéficier de l'héritage multiple : Bell et Restart possèdent toutes les méthodes d'Event, mais elles semblent disposer également de toutes les méthodes de la classe externe GreenhouseControls.

Restart est un tableau d'objet Event qui est ajouté au contrôleur. Puisque Restart( ) n'est qu'un objet Event comme un autre, on peut aussi ajouter un objet Restart depuis Restart.action() afin que le système se relance de lui-même régulièrement.

La classe suivante configure le système par la création d'un objet GreenhouseControls et ajoute différents types d'objets Event . C'est un exemple du design pattern Command :

 
Sélectionnez
        //: c08:GreenhouseController.java
        // Configure et exécute le système de serre.
        // {Args: 5000}
        import c08.controller.*;

        public class GreenhouseController {
            public static void main(String[] args) {
            GreenhouseControls gc = new GreenhouseControls();
            // Au lieu de coder en dur, vous pouvez parser les 
            // informations de configuration depuis un fichier texte ici :
            gc.addEvent(gc.new Bell(900));
            Event[] eventList = {
                gc.new ThermostatNight(0),
                gc.new LightOn(200),
                gc.new LightOff(400),
                gc.new WaterOn(600),
                gc.new WaterOff(800),
                gc.new ThermostatDay(1400)
            };
            gc.addEvent(gc.new Restart(2000, eventList));
            if(args.length == 1)
                gc.addEvent(
            gc.new Terminate(Integer.parseInt(args[0])));
            gc.run();
            }
        } ///:~

Cette classe initialise le système, ainsi elle ajoute tous les événements appropriés. Bien sûr, un moyen plus souple d'accomplir ceci est d'éviter le codage en dur des événements et à la place les lire depuis un fichier. (Un exercice dans le chapitre 12 vous demande de modifier cet exemple pour réaliser ceci.) Si vous fournissez un argument en ligne de commande, elle l'utilise pour terminer le programme après quelques millisecondes (ce qui est utilisé pour les essais).

Cet exemple vous amènera à apprécier à leur juste valeur l'utilisation des classes internes, en particulier lorsqu'elles sont utilisées dans un cadre de contrôle. Toutefois, au chapitre 14, vous verrez comment les classes internes sont utilisées avec élégance pour décrire les actions de l'interface graphique de l'utilisateur. Au moment où vous avez terminé ce chapitre, vous devriez être pleinement convaincu.

VIII-D. Summary

Les interfaces et les classes internes sont des concepts plus sophistiqués que ce que vous pourrez trouver dans beaucoup de langages de programmation orientés objet. Par exemple, rien de comparable n'existe en C++. Ensemble, elles résolvent le même problème que celui que le C++ tente de résoudre avec les fonctionnalités de l'héritage multiple. Cependant, l'héritage multiple en C++ se révèle relativement ardu à mettre en œuvre, tandis que les interfaces et les classes internes en Java sont, en comparaison, d'un abord nettement plus facile.

Bien que les fonctionnalités en elles-mêmes soient relativement simples, leur utilisation relève de la conception, de même que le polymorphisme. Avec le temps, vous reconnaîtrez plus facilement les situations dans lesquelles utiliser une interface, ou une classe interne, ou les deux. Mais à ce point du livre vous devriez au moins vous sentir à l'aise avec leur syntaxe et leur sémantique. Vous intègrerez ces techniques au fur et à mesure que vous les verrez utilisées.

VIII-E. Exercices

Les solutions des exercices sélectionnés sont disponibles dans le document électronique The Thinking in Java Annotated Solution Guide, disponible pour un faible coût sur www.BruceEckel.com.

  1. Prouver que les champs d'une interface sont implicitement static et final.
  2. Créer une interface contenant trois méthodes, dans son propre package. Implémenter cette interface dans un package différent.
  3. Prouver que toutes les méthodes d'une interface sont automatiquement public.
  4. Dans c07:Sandwich.java, créer une interface appelée FastFood (avec les méthodes appropriées) et changer Sandwich afin qu'il implémente FastFood.
  5. Créer trois interfaces, chacune avec deux méthodes. Créer une nouvelle interface héritant des trois, en ajoutant une nouvelle méthode. Créer une classe implémentant la nouvelle interface et héritant déjà d'une classe concrète. Écrire maintenant quatre méthodes, chacune d'entre elles prenant l'une des quatre interfaces en argument. Dans main( ), créer un objet de votre classe et le passer à chacune des méthodes.
  6. Modifier l'exercice 5 en créant une classe abstract et en la dérivant dans la dernière classe.
  7. Modifier Music5.java en ajoutant une interface Playable. Déplacer la déclaration de play( ) depuis Instrument vers Playable. Ajouter Playable à la classe dérivée en l'incluant dans la liste implements. Modifier tune( ) afin qu'il accepte un Playable au lieu d'un Instrument.
  8. Changer l'exercice 6 du Chapitre 7 afin que Rodent soit une interface.
  9. Dans Adventure.java, ajouter une interface appelée CanClimb, respectant la forme des autres interfaces.
  10. Écrire un programme qui importe et utilise Month.java.
  11. En suivant l'exemple donné dans Month.java, créer une énumération des jours de la semaine.
  12. Créer une interface dans son propre package contenant au moins une méthode. Créer une classe dans un package séparé. Ajouter une classe interne protected qui implémente l'interface. Dans un troisième package, dériver votre classe, et dans une méthode renvoyer un objet de la classe interne protected, en le transtypant en interface lors du retour.
  13. Créer une interface contenant au moins une méthode, et implémenter cette interface en définissant une classe interne à l'intérieur d'une méthode, qui renvoie une référence sur votre interface.
  14. Répéter l'exercice 13, mais définir la classe interne à l'intérieur d'un champ d'application d'une méthode.
  15. Répéter l'exercice 13 en utilisant une classe interne anonyme.
  16. Modifier HorrorShow.java pour implémenter DangerousMonster et Vampire en utilisant des classes anonymes.
  17. Créer une classe interne private qui implémente une interface public. Écrire une méthode qui renvoie une référence sur une instance de la classe interne private, transtypée (ascendant) en interface. Montrer que la classe interne est complètement cachée en essayant de faire un transtypage descendant.
  18. Créer une classe avec un constructeur autre que celui par défaut (un avec arguments) et sans constructeur par défaut (pas de constructeur sans argument). Créer une seconde classe ayant une méthode qui renvoie une référence à la première classe. Créer un objet à renvoyer en créant une classe interne anonyme dérivée de la première classe.
  19. Créer une classe avec un champ private et une méthode private. Créer une classe interne avec une méthode qui modifie le champ de la classe externe et appelle la méthode de la classe externe. Dans une seconde méthode de la classe externe, créer un objet de la classe interne et appeler sa méthode ; montrer alors l'effet sur l'objet de la classe externe.
  20. Répéter l'exercice 19 en utilisant une classe interne anonyme.
  21. Créer une classe contenant une classe imbriquée. Dans le main( ), créer une instance de la classe interne.
  22. Créer une interface contenant une classe imbriquée. Implémenter cette interface et créer une instance de la classe imbriquée.
  23. Créer une classe contenant une classe interne contenant elle-même une classe interne. Répéter ce schéma en utilisant des classes imbriquées. Noter les noms des fichiers .class produits par le compilateur.
  24. Créer une classe avec une classe interne. Dans une classe séparée, créer une instance de la classe interne.
  25. Créer une classe avec une classe interne disposant d'un constructeur autre que celui par défaut (un constructeur qui prend des arguments). Créer une seconde classe avec une classe interne qui hérite de la première classe interne.
  26. Corriger le problème dans WindError.java.
  27. Modifier Sequence.java en ajoutant une méthode getRSelector( ) qui produit une implémentation différente de l'interface Selector afin de parcourir la séquence en ordre inverse, de la fin vers le début.
  28. Créer une interface U contenant trois méthodes. Créer une classe A avec une méthode qui produit une référence sur un U en construisant une classe interne anonyme. Créer une seconde classe B qui contient un tableau de U. B doit avoir une méthode qui accepte et stocke une référence sur un U dans le tableau, une deuxième méthode qui positionne une référence (spécifiée par un argument de la méthode) dans le tableau à null, et une troisième méthode qui se déplace dans le tableau et appelle les méthodes de U. Dans main( ), créer un groupe d'objets A et un objet B. Remplir l'objet B avec les références U produites par les objets A. Utiliser B pour revenir dans tous les objets A. Supprimer certaines des références U de B.
  29. Dans GreenhouseControls.java, ajouter des classes internes Event qui allument et éteignent des ventilateurs. Configurer GreenhouseController.java pour utiliser ces nouveaux objets Event.
  30. Hériter de GreenhouseControls dans GreenhouseControls.java afin d'ajouter des classes internes Event qui allument et éteignent les générateurs de vapeur d'eau. Écrire une nouvelle version de GreenhouseController.java pour utiliser ces nouveaux objets Event.
  31. Montrer qu'une classe interne a accès aux éléments private de sa classe externe. Déterminer si l'inverse est vrai.

précédentsommairesuivant
This approach was inspired by an e-mail from Rich Hoffarth. Item 21 in Joshua Bloch's Effective Java (Addison-Wesley, 2001) covers the topic in much more detail.
Merci à Martin Danner d’avoir posé ces questions durant un séminaire.
Ceci est très différent du concept des classes imbriquées en C++, qui est simplement un mécanisme de camouflage de noms. Il n'y a aucun lien avec l'objet externe et aucune permission implicite en C++
Roughly similar to nested classes in C++, except that those classes cannot access private members as they can in Java.
Encore merci à Martin Danner.
D'autre part, '$' est un métacaractère pour les shells unix et vous pourrez parfois rencontrer des difficultés en listant les fichiers .class. Ceci peut paraître un peu étrange de la part de Sun, une entreprise tournée vers Unix. Je pense qu'ils n'ont pas pris en compte ce problème, car ils pensaient que l'attention se porterait surtout sur les fichiers de code source.
Pour une certaine raison, cela a toujours été pour moi un problème plaisant à résoudre ; cela vient de mon précédent livre C++ Inside & Out, mais Java permet une solution bien plus élégante.

Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.