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 12 - Identification dynamique de type

pages : 1 2 3 

La classe Class (décrite précédemment dans ce chapitre) supporte le concept de réflexion, et une bibliothèque additionnelle, java.lang.reflect, contenant les classes FieldMethod, et Constructor (chacune implémentant l'interface Member). Les objets de ce type sont créés dynamiquement par la JVM pour représenter les membres correspondants d'une classe inconnue. On peut alors utiliser les constructeurs pour créer de nouveaux objets, les méthodes get() et set() pour lire et modifier les champs associés à des objets Field, et la méthode invoke() pour appeler une méthode associée à un objet Method. De plus, on peut utiliser les méthodes très pratiques getFields(), getMethods(), getConstructors(), etc. retournant un tableau représentant respectivement des champs, méthodes et constructeurs (pour en savoir plus, jetez un oeil à la documentation en ligne de la classe Class). Ainsi, l'information sur la classe d'objets inconnus peut être totalement déterminée dynamiquement, sans rien en savoir à la compilation.

Il est important de noter qu'il n'y a rien de magique dans la réflexion. Quand vous utilisez la réflexion pour interagir avec des objets de type inconnu, la JVM va simplement regarder l'objet et voir qu'il appartient à une classe particulière (comme une RTTI ordinaire) mais, avant toute autre chose, l'objet Class doit être chargé. Le fichier .class pour ce type particulier doit donc être disponible pour la JVM, soit localement sur la machine ou via le réseau. La vraie différence entre le RTTI et la réflexion est donc qu'avec le RTTI, le compilateur ouvre et examine le fichier .class à la compilation. Dit autrement, vous pouvez appeler toutes les méthodes d'un objet “normalement”. Avec la réflexion, le fichier .class n'est pas disponible à la compilation ; il est ouvert et examiné à l'exécution.

Un extracteur de méthodes de classe

Vous aurez rarement besoin d'utiliser directement les outils de réflexion ; ils sont utilisés pour supporter d'autres caractéristiques de Java, telles que la sérialisation (Chapitre 11), JavaBeans (Chapitre 13) et RMI (Chapitre 15). Néanmoins, il est quelquefois utile d'extraire dynamiquement des informations sur une classe. Un outil très utile est un extracteur de méthode de classe. Comme mentionné précédemment, chercher le code définissant une classe ou sa documentation en ligne montre uniquement les méthodes définies ou redéfinies dans cette définition de classe. Mais il peut y en avoir des douzaines d'autre qui proviennent des classes de base. Les retrouver est fastidieux et long  [60]. Heureusement, la réflexion fournit un moyen d'écrire un outil simple qui va automatiquement montrer l'interface entière. Voici comment il fonctionne :

//: c12:ShowMethods.java
// Utiliser la réflexion pour montrer toutes les méthodes
// d'une classe, même si celles ci sont définies dans la
// classe de base.
import java.lang.reflect.*;

public class ShowMethods {
  static final String usage =    "usage: \n" +
    "ShowMethods qualified.class.name\n" +
    "Pour montrer toutes les méthodes or: \n" +
    "ShowMethods qualified.class.name word\n" +
    "Pour rechercher les méthodes contenant 'word'";
  public static void main(String[] args) {
    if(args.length < 1) {
      System.out.println(usage);
      System.exit(0);
    }
    try {
      Class c = Class.forName(args[0]);
      Method[] m = c.getMethods();
      Constructor[] ctor = c.getConstructors();
      if(args.length == 1) {
        for (int i = 0; i < m.length; i++)
          System.out.println(m[i]);
        for (int i = 0; i < ctor.length; i++)
          System.out.println(ctor[i]);
      } else {
        for (int i = 0; i < m.length; i++)
          if(m[i].toString()
             .indexOf(args[1])!= -1)
            System.out.println(m[i]);
        for (int i = 0; i < ctor.length; i++)
          if(ctor[i].toString()
             .indexOf(args[1])!= -1)
          System.out.println(ctor[i]);
      }
    } catch(ClassNotFoundException e) {
      System.err.println("Classe non trouvée : " + e);
    }
  }
} ///:~

Les méthodes de Class getMethods() et getConstructors() retournent respectivement un tableau de Method et Constructor. Chacune de ces classes a de plus des méthodes pour obtenir les noms, arguments et valeur retournée des méthodes qu'elles représentent. Mais vous pouvez aussi utiliser simplement toString(), comme ici, pour produire une chaîne de caractères avec la signature complète de la méthode. Le reste du code sert juste pour l'extraction des informations de la ligne de commande, déterminer si une signature particulière correspond à votre chaîne cible (en utilisant indexOf()), et afficher le résultat.

Ceci montre la réflexion en action, puisque le résultat de Class.forName() ne peut pas être connu à la compilation, donc toutes les informations sur la signature des méthodes est extraite à l'exécution. Si vous étudiez la documentation en ligne sur la réflexion, vous verrez qu'il est possible de créér et d'appeler une méthode d'un objet qui était totalement inconnu lors de la compilation (nous verrons des exemples plus loin dans ce livre). Encore une fois, c'est quelque chose dont vous n'aurez peut être jamais besoin de faire vous même- le support est là pour le RMI et la programmation par JavaBeans- mais il est intéressant.

Une expérience intéressante est de lancer :

java ShowMethods ShowMethods

Ceci produit une liste qui inclut un constructeur par défaut public, bien que vous puissiez voir à partir du code source qu'aucun constructeur n'ait été défini. Le constructeur que vous voyez est celui qui est automatiquement généré par le compilateur. Si vous définissez maintenant ShowMethods comme une classe non public (par exemple, amie), le constructeur par défaut n'apparaît plus dans la liste. Le constructeur pas défaut généré a automatiquement le même accès que la classe.

L'affichage de ShowMethods est toujours un peu ennuyeuse. Par exemple, voici une portion de l'affichage produit en invoquant  java ShowMethods java.lang.String :

public boolean
  java.lang.String.startsWith(java.lang.String,int)
public boolean
  java.lang.String.startsWith(java.lang.String)
public boolean
  java.lang.String.endsWith(java.lang.String)

Il serait préférable que les préfixes comme java.lang puissent être éliminés. La classe StreamTokenizer introduite dans le chapitre précédent peut nous aider à créer un outil résolvant ce problème :

//: com:bruceeckel:util:StripQualifiers.java
package com.bruceeckel.util;
import java.io.*;

public class StripQualifiers {
  private StreamTokenizer st;
  public StripQualifiers(String qualified) {
    st = new StreamTokenizer(
      new StringReader(qualified));
    st.ordinaryChar(' '); // garde les espaces
  }
  public String getNext() {
    String s = null;
    try {
      int token = st.nextToken();
      if(token != StreamTokenizer.TT_EOF) {
        switch(st.ttype) {
          case StreamTokenizer.TT_EOL:
            s = null;
            break;
          case StreamTokenizer.TT_NUMBER:
            s = Double.toString(st.nval);
            break;
          case StreamTokenizer.TT_WORD:
            s = new String(st.sval);
            break;
          default: //il y a un seul caractère dans ttype
            s = String.valueOf((char)st.ttype);
        }
      }
    } catch(IOException e) {
      System.err.println("Erreur recherche token");
    }
    return s;
  }
  public static String strip(String qualified) {
    StripQualifiers sq =
      new StripQualifiers(qualified);
    String s = "", si;
    while((si = sq.getNext()) != null) {
      int lastDot = si.lastIndexOf('.');
      if(lastDot != -1)
        si = si.substring(lastDot + 1);
      s += si;
    }
    return s;
  }
} ///:~

Pour faciliter sa réutilisation, cette classe est placée dans com.bruceeckel.util. Comme vous pouvez le voir, elle utilise la classe StreamTokenizer et la manipulation des String pour effectuer son travail.

La nouvelle version du programme utilise la classe ci-dessus pour clarifier le résultat :

//: c12:ShowMethodsClean.java
// ShowMethods avec élimination des préfixes
// pour faciliter la lecture du résultat.
import java.lang.reflect.*;
import com.bruceeckel.util.*;

public class ShowMethodsClean {
  static final String usage =    "usage: \n" +
    "ShowMethodsClean qualified.class.name\n" +
    "Pour montrer toutes les méthodes or: \n" +
    "ShowMethodsClean qualified.class.name word\n" +
    "Pour rechercher les méthodes contenant 'word'";

  public static void main(String[] args) {
    if(args.length < 1) {
      System.out.println(usage);
      System.exit(0);
    }
    try {
      Class c = Class.forName(args[0]);
      Method[] m = c.getMethods();
      Constructor[] ctor = c.getConstructors();
      // Conversion en un tableau de chaînes simplifiées :
      String[] n =
        new String[m.length + ctor.length];
      for(int i = 0; i < m.length; i++) {
        String s = m[i].toString();
        n[i] = StripQualifiers.strip(s);
      }
      for(int i = 0; i < ctor.length; i++) {
        String s = ctor[i].toString();
        n[i + m.length] =
          StripQualifiers.strip(s);
      }
      if(args.length == 1)
        for (int i = 0; i < n.length; i++)
          System.out.println(n[i]);
      else
              for (int i = 0; i < n.length; i++)
          if(n[i].indexOf(args[1])!= -1)
            System.out.println(n[i]);
    } catch(ClassNotFoundException e) {
      System.err.println("Classe non trouvée : " + e);
    }
  }
} ///:~

La classe ShowMethodsClean est semblable à la classe ShowMethods, excepté qu'elle transforme les tableaux de Method et Constructor en un seul tableau de String. Chaque String est ensuite appliquée à StripQualifiers.strip() pour enlever les préfixes des méthodes.

Cet outil peut réellement vous faire gagner du temps lorsque vous programmez, quand vous ne vous souvenez pas si une classe a une méthode particulière et que vous ne voulez pas explorer toute sa hiérarchie dans la documentation en ligne, ou si vous ne savez pas si cette classe peut faire quelque chose avec, par exemple, un objet Color.

Le chapitre 13 contient une version graphique de ce programme (adapté pour extraire des informations sur les composants Swing) que vous pouvez laisser tourner pendant que vous écrivez votre code, pour des recherches rapides.

Résumé

L'identification dynamique de type (RTTI) permet de découvrir des informations de type à partir d'une référence sur une classe de base inconnue. [je n'arrive pas à traduire cette phrase: Thus, it’s ripe for misuse by the novice since it might make sense before polymorphic method calls do. Prop1:Ainsi, il mûrit pour sa mauvaise utilisation par le novice puisque il pourrait être utilise de le faire avant un appel de méthode polymorphique. Prop2 (JQ): Malheureusement ces informations peuvent conduire le novice à négliger les concepts du polymorphisme, puisqu'elles sont plus faciles à appréhender.] Pour beaucoup de gens habitués à la programmation procédurale, il est difficile de ne pas organiser leurs programmes en ensembles d'expressions switch. Ils pourraient faire la même chose avec le RTTI et perdraient ainsi l'importante valeur du polymorphisme dans le développement et la maintenance du code. L'intention de Java est de vous faire utiliser des appels de méthodes polymorphiques dans votre code, et de vous faire utiliser le RTTI uniquement lorsque c'est nécessaire.

Néanmoins, utiliser des appels de méthodes polymorphiques nécessite que vous ayez le contrôle de la définition des classes de base car il est possible que lors du développement de votre programme vous découvriez que la classe de base ne contient pas une méthode dans vous avez besoin. Si la classe de base provient d'une bibliothèque ou si elle est contrôlée par quelqu'un d'autre, une solution à ce problème est le RTTI : vous pouvez créer un nouveau type  héritant de cette classe auquel vous ajoutez la méthode manquante. Ailleurs dans le code, vous détectez ce type particulier et appelez cette méthode spécifique. Ceci ne détruit ni le polymorphisme ni l'extensibilité du programme car ajouter un nouveau type ne vous oblige pas à chasser les expressions switch dans votre programme. Cependant, lorsque vous ajoutez du code qui requiert cette nouvelle fonctionnalité dans votre programme principal, vous devez utiliser le RTTI pour détecter ce type particulier.

Mettre la nouvelle caractéristique dans la classe de base peut signifier que, pour le bénéfice d'une classe particulière, toutes les autres classes dérivées de cette classe de base devront contenir des bouts de code inutiles de la méthode. Cela rend l'interface moins claire et ennuie celui qui doit redéfinir des méthodes abstraites dérivant de la classe de base. Supposez que vous désiriez nettoyer les becs? [spit valves] de tous les instruments à vent de votre orchestre. Une solution est de mettre une méthode nettoyerBec() dans la classe de base Instrument, mais c'est ennuyeux car cela implique que les instruments Electroniques et à Percussion ont aussi un bec. Le RTTI fournit une solution plus élégante dans ce cas car vous pouvez placer la méthode dans une classe spécifique (Vent dans notre cas), où elle est appropriée. Néanmoins, une solution encore meilleure est de mettre une méthode prepareInstrument() dans la classe de base, mais il se peut que vous ne la trouviez pas la première fois que vous ayez à résoudre le problème, et croyiez à tort que l'utilisation du RTTI est nécessaire.

Enfin, le RTTI permettra parfois de résoudre des problèmes d'efficacité. Si votre code utilise le polymorphisme, mais qu'il s'avère que l'un de vos objets réagisse à ce code très général d'une manière particulièrement inefficace, vous pouvez reconnaître ce type en utilisant le RTTI et écrire un morceau de code spécifique pour améliorer son efficacité. Attention toutefois à programmer pour l'efficacité  trop tôt. C'est un piège séduisant. Il est préférable d'avoir un programme qui marche d'abord, et décider ensuite s'il est assez rapide, et seulement à ce moment là vous attaquer aux problèmes de performances — avec un profiler.

Exercices

Les solutions de certains exercices se trouvent dans le document électronique Guide des solutions annoté de Penser en Java, disponible à prix réduit depuis www.BruceEckel.com.

  1. Ajouter Rhomboïde àFormes.java. Créer un Rhomboïde, le transtyper en une Forme, ensuite le re-transtyper en un Rhomboïde. Essayer de le transtyper en un Cercle et voir ce qui se passe.
  2. Modifier l'exercice 1 afin d'utiliser instanceof pour vérifier le type avant d'effectuer le transtypage descendant.
  3. Modifier Formes.java afin que toutes les formes d'un type particulier puissent être mises en surbrillance (utiliser un drapeau). La méthode toString() pour chaque classe dérivée de Forme devra indiquer si cette Forme est mise en surbrillance ou pas.
  4. Modifier Confiseur.java afin que la création de chaque type d'objet soit contrôlée par la ligne de commande. Par exemple, si la ligne de commande est “java Confiseur Bonbon”, seul l'objet Bonbon sera créé. Noter comment vous pouvez contrôler quels objets Class sont chargés via la ligne de commande.
  5. Ajouter un nouveau type de Pet à PetCount3.java. Vérifier qu'il est correctement créé et compté dans la méthode main().
  6. Écrire une méthode qui prend un objet en paramètre et affiche récursivement tous les classes de sa hiérarchie.
  7. Modifier l'exercice 6 afin d'utiliser Class.getDeclaredFields() pour afficher aussi les informations sur les champs de chaque classe.
  8. Dans ToyTest.java, commenter le constructeur par défaut de Toy et expliquer ce qui arrive.
  9. Ajouter une nouvelle interface dans ToyTest.java et vérifier qu'elle est correctement détectée et affichée.
  10. Créer un nouveau type de conteneur qui utilise une private ArrayList pour stocker les objets. Déterminer le type du premier objet déposé, ne permettre ensuite à l'utilisateur que d'insérer des objets de ce type.
  11. Écrire un programme qui détermine si un tableau de char est un type primitif ou réellement un objet.
  12. Implanter nettoyerBec( ) comme décrit dans le résumé.
  13. Modifier l'exercice 6 afin d'utiliser la réflexion à la place du RTTI.
  14. Modifier l'exercice 7 afin d'utiliser la réflexion à la place du RTTI.
  15. Dans ToyTest.java, utiliser la réflexion pour créer un objet Toy en n'utilisant pas le constructeur par défaut.
  16. Étudier l'interface java.lang.Class dans la documentation HTML de Java à java.sun.com. Écrire un programme qui prend en paramètre le nom d'une classe via la ligne de commande, et utilise les méthodes de Class pour extraire toutes les informations disponibles pour cette classe. Tester le programme sur une classe de la bibliothèque standard et sur une des vôtres.

[60] Spécialement dans le passé. Néanmoins, Sun a grandement amélioré la documentation HTML de Java et il est maintenant plus aisé de voir les méthodes des classes de base.

Ce livre a été écrit par Bruce Eckel ( télécharger la version anglaise : Thinking in java )
Ce chapitre a été traduit par Daniel Le Berre ( 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 
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