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 

« 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 :

Image

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 :

//: 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.

Combinaison d'interfaces et collisions de noms 

On peut renconter 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 :

//: 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 toutes façons, utiliser les mêmes noms de méthode dans différentes interfaces destinées à être combinées affecte la compréhension du code. Il faut donc l'éviter autant que faire se peut.

Etendre 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 :

//: 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 HorrorShow {
  static void u(Monster b) { b.menace(); }
  static void v(DangerousMonster d) {
    d.menace();
    d.destroy();
  }
  public static void main(String[] args) {
    DragonZilla if2 = new DragonZilla();
    u(if2);
    v(if2);
  }
} ///:~

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 on peut le voir, les noms d'interface sont simplement séparées par des virgules.

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 :

//: 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  [38]:

//: c08:Month2.java
// Un système d'énumération plus robuste.
package c08;

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

Cette classe est appelée Month2, puisque Month existe déjà dans la bibliothèque Java standard. C'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, qui permet de choisir les mois par leur index au lieu de leur nom (notez le premier JAN dans le tableau pour introduire un déplacement supplémentaire de un, afin que Décembre soit le mois numéro 12). Dans main() on dispose de la vérification additionnelle de type : m est un objet Month2 et ne peut donc se voir assigné qu'un Month2. 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().

Initialisation des données membres des 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 :

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

public interface RandVals {
  int rint = (int)(Math.random() * 10);
  long rlong = (long)(Math.random() * 10);
  float rfloat = (float)(Math.random() * 10);
  double rdouble = Math.random() * 10;
} ///:~

Comme 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 :

//: c08:TestRandVals.java

public class TestRandVals {
  public static void main(String[] args) {
    System.out.println(RandVals.rint);
    System.out.println(RandVals.rlong);
    System.out.println(RandVals.rfloat);
    System.out.println(RandVals.rdouble);
  }
} ///:~

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.

Interfaces imbriquées

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

//: 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 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() :
    A 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). A 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.

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. A 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 :

//: 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 :

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

Classes internes et transtypage ascendant

Jusqu'à présent, les classes internes ne semblent pas tellement intéressantes. 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 rendre la classe « amicale » (visible seulement depuis un certain package) plutôt que de la déclarer comme une classe interne.

Cependant, les classes internes prennent de l'intérêt lorsqu'on transtype 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). En effet la classe interne - l'implémentation de l'interface - est 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 :

//: c08:Destination.java
public interface Destination {
  String readLabel();
} ///:~

//: c08:Contents.java
public interface Contents {
  int value();
} ///:~

Maintenant Contents et Destination sont des interfaces disponibles pour le programmeur client (une interface déclare automatiquement tous ses membres comme 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 :

//: c08:Parcel3.java
// Renvoyer une référence sur une classe interne.

public 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();
  }
}

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

Notez que puisque main() se trouve dans Test, pour lancer ce programme il ne faut pas exécuter Parcel3, mais :

java Test

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.

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