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

  Chapitre 8 - Interfaces et classes internes

pages : 1 2 3 4 5 6 

Les classes internes static diffèrent aussi des classes internes non static d'une autre manière. Les champs et les méthodes des classes internes non static ne peuvent être qu'au niveau externe de la classe, les classes internes non static ne peuvent donc avoir de données static, de champs static ou de classes internes static. Par contre, les classes internes static peuvent avoir tout cela :

//: c08:Parcel10.java
// Classes internes static.

public class Parcel10 {
  private static class PContents
  implements Contents {
    private int i = 11;
    public int value() { return i; }
  }
  protected static class PDestination
      implements Destination {
    private String label;
    private PDestination(String whereTo) {
      label = whereTo;
    }
    public String readLabel() { return label; }
    // Les classes internes static peuvent
    // contenir d'autres éléments static :
    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 PDestination(s);
  }
  public static Contents cont() {
    return new PContents();
  }
  public static void main(String[] args) {
    Contents c = cont();
    Destination d = dest("Tanzania");
  }
} ///:~

Dans main(), aucun objet Parcel10 n'est nécessaire ; on utilise à la place la syntaxe habituelle pour sélectionner un membre static pour appeler les méthodes qui renvoient des références sur Contents et Destination.

Comme on va le voir bientôt, dans une classe interne ordinaire (non static), le lien avec la classe externe est réalisé avec une référence spéciale this. Une classe interne static ne dispose pas de cette référence spéciale this, ce qui la rend analogue à une méthode static.

Normalement, on ne peut placer du code à l'intérieur d'une interface, mais une classe interne static peut faire partie d'une interface. Comme la classe est static, cela ne viole pas les règles des interfaces - la classe interne static est simplement placée dans l'espace de noms de l'interface :

//: c08:IInterface.java
// Classes internes static à l'intérieur d'interfaces.

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

Plus tôt dans ce livre je suggérais de placer un main() dans chaque classe se comportant comme un environnement de tests pour cette classe. Un inconvénient de cette approche est le volume supplémentaire de code compilé qu'on doit supporter. Si cela constitue un problème, on peut utiliser une classe interne static destinée à contenir le code de test :

//: c08:TestBed.java
// Code de test placé dans une classe interne static.

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

Ceci génère une classe séparée appelée TestBed$Tester (pour lancer le programme, il faut utiliser la commande java TestBed$Tester). On peut utiliser cette classe lors des tests, mais on n'a pas besoin de l'inclure dans le produit final.

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).

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

//: 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 ; à la place il faut utiliser un objet de la classe externe pour créer un objet de la classe interne :

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, parce qu'un objet de la classe interne est toujours connecté avec l'objet de la classe externe qui l'a créé. Cependant, si la classe interne est static, elle n'a pas besoin d'une référence sur un objet de la classe externe.

Classe interne à plusieurs niveaux d'imbrication

[41]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 :

//: 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.A 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 qualification (malgré le fait qu'elles soient private). Cet exemple présente aussi la syntaxe utilisée 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 et on n'a donc pas besoin de qualifier le nom de la classe dans l'appel du constructeur.

Dériver une classe interne

Comme le constructeur d'une classe interne doit stocker 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 sur lequel se rattacher par défaut. Il faut donc utiliser une syntaxe qui rende cette association explicite :

//: c08:InheritInner.java
// Inheriting an inner class.

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 lorsqu'on en arrive au constructeur, celui fourni par défaut n'est pas suffisant et on ne peut se contenter de passer une référence à un objet externe. De plus, on doit utiliser la syntaxe :

enclosingClassReference.super();

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

Les classes internes peuvent-elles redéfinies ?

Que se passe-t-il quand on crée une classe interne, qu'on dérive la classe externe et qu'on redéfinit la classe interne ? Autrement dit, est-il possible de rédéfinir une classe interne ? Ce concept semble particulièrement puissant, mais « redéfinir » une classe interne comme si c'était une méthode de la classe externe ne fait rien de spécial :

//: c08:BigEgg.java
// Une classe interne ne peut être
// redéfinie comme une méthode.

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

public class BigEgg extends Egg {
  public class Yolk {
    public Yolk() {
      System.out.println("BigEgg.Yolk()");
    }
  }
  public static void main(String[] args) {
    new BigEgg();
  }
} ///:~

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, la version « redéfinie » de Yolk sera utilisée, mais ce n'est pas le cas. La sortie produite est :

New Egg()
Egg.Yolk()

Cet exemple montre simplement 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 de dériver explicitement la classe interne :

//: c08:BigEgg2.java
// Dérivation d'une classe interne.

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 {
  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();
  }
} ///:~

Maintenant BiggEgg2.Yolk étend explicitement Egg2.Yolk et redéfinit ses méthodes. La méthode insertYolk() permet à BiggEgg2 de transtyper 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. La sortie du programme est :

Egg2.Yolk()
New Egg2()
Egg2.Yolk()
BigEgg2.Yolk()
BigEgg2.Yolk.f()

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.

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éta-classe » dans un objet Class), il est aisé de 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 InheritInner.java incluent :

InheritInner.class
WithInner$Inner.class
WithInner.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 robuste et gère la plupart des situations [42]. 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).

Raison d'être des classes internes

Jusqu'à présent, on a vu la syntaxe 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éé. On peut donc dire qu'une classe interne est une sorte de fenêtre dans la classe externe.

Mais si on a 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 coeur des classes internes est simple : « Si c'est tout ce dont on a 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, certaines conceptions ou problèmes 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 permettent effectivement de dériver plusieurs non interfaces.

Pour voir ceci plus en détails, imaginons une situation dans laquelle une classe doit implémenter deux interfaces. Du fait de la flexibilité des interfaces, on a le choix entre avoir une classe unique ou s'aider d'une classe interne :

//: 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, la structure du code peut impliquer une logique pouvant imposer l'une ou l'autre des solutions. La nature du problème fournit généralement aussi des indices pour choisir entre une classe unique ou une classe interne. Mais en l'absence d'aucune autre contrainte, l'approche choisie dans l'exemple précédent ne fait aucune différence du point de vue implémentation. Les deux fonctionnent.

Cependant, si on a des classes abstract ou concrètes à la place des interfaces, on est obligé de recourir aux classes internes si la classe doit implémenter les deux :

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