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 

Le principe de l'identification dynamique de type (Run-Time Type Identification, RTTI) semble très simple à première vue : connaître le type exact d'un objet à partir d'une simple référence sur un type de base.

Néanmoins, le besoin de RTTI dévoile une pléthore de problèmes intéressants (et souvent complexes)  en conception orientée objet, et renforce la question fondamentale de comment structurer ses programmes.

Ce chapitre indique de quelle manière Java permet de découvrir dynamiquement des informations sur les objets et les classes. On le retrouve sous deux formes : le RTTI « classique », qui suppose que tous les types sont disponibles à la compilation et à l'exécution, et le mécanisme de « réflexion », qui permet de découvrir des informations sur les classes uniquement à l'exécution. Le RTTI « classique » sera traité en premier, suivi par une discussion sur la réflexion.

Le besoin de RTTI

Revenons à notre exemple d'une hiérarchie de classes utilisant le polymorphisme. Le type générique est la classe de base Forme, et les types spécifiques dérivés sont Cercle, Carre et Triangle :

Image

C'est un diagramme de classe hiérarchique classique, avec la classe de base en haut et les classes dérivées qui en découlent. Le but usuel de la programmation orientée objet est de manipuler dans la majorité du code des références sur le type de base (Forme, ici), tel que si vous décidez de créer une nouvelle classe (Rhomboïde, dérivée de Forme, par exemple), ce code restera inchangé. Dans notre exemple, la méthode liée dynamiquement dans l'interface Forme est draw(), ceci dans le but que le programmeur appelle draw() à partir d'une référence sur un objet de type Forme. draw() est redéfinie dans toutes les classes dérivées, et parce que cette méthode est liée dynamiquement, le comportement attendu arrivera même si l'appel se fait à partir d'une référence générique sur Forme. C'est ce que l'on appelle le polymorphisme.

Ainsi, on peut créer un objet spécifique (Cercle, Carre ou Triangle), le transtyper à Forme (oubliant le type spécifique de l'objet), et utiliser une référence anonyme à Forme dans le reste du programme.

Pour avoir un bref aperçu du polymorphisme et du transtypage ascendant (upcast), vous pouvez coder l'exemple ci-dessous :

//: c12:Formes.java
import java.util.*;

class Forme {
  void draw() {
    System.out.println(this + ".draw()");
  }
}

class Cercle extends Forme {
  public String toString() { return "Cercle"; }
}

class Carre extends Forme {
  public String toString() { return "Carre"; }
}

class Triangle extends Forme {
  public String toString() { return "Triangle"; }
}

public class Formes {
  public static void main(String[] args) {
    ArrayList s = new ArrayList();
    s.add(new Cercle());
    s.add(new Carre());
    s.add(new Triangle());
    Iterator e = s.iterator();
    while(e.hasNext())
      ((Shape)e.next()).draw();
  }
} ///:~

La classe de base contient une méthode draw() qui utilise indirectement toString() pour afficher un identifiant de la classe en utilisant this en paramètre de System.out.println(). Si cette fonction rencontre un objet, elle appelle automatiquement la méthode toString() de cet objet pour en avoir une représentation sous forme de chaîne de caractères.

Chacune des classes dérivées redéfinit la méthode toString() (de la classe Object) pour que draw() affiche quelque chose de différent dans chaque cas. Dans main(), des types spécifiques de Forme sont créés et ajoutés dans un ArrayList. C'est à ce niveau que le transtypage ascendant intervient car un ArrayList contient uniquement des Objects. Comme tout en Java (à l'exception des types primitifs) est Object, un ArrayList peut aussi contenir des Formes. Mais lors du transtypage en Object, il perd toutes les informations spécifiques des objets, par exemple que ce sont des Formes. Pour le ArrayList, ce sont juste des Objects.

Lorsqu'on récupère ensuite un élément de l'ArrayList avec la méthode next(), les choses se corsent un peu. Comme un ArrayList contient uniquement des Objects, next() va naturellement renvoyer une référence sur un Object. Mais nous savons que c'est en réalité une référence sur une Forme, et  nous désirons envoyer des messages de Forme à cet objet. Donc un transtypage en Forme est nécessaire en utilisant le transtypage habituel « (Forme) ». C'est la forme la plus simple de RTTI, puisqu'en Java l'exactitude de tous les typages est vérifiée à l'exécution. C'est exactement ce que signifie RTTI : à l'exécution, le type de tout objet est connu.

Dans notre cas, le RTTI est seulement partiel : l'Object est transtypé en Forme, mais pas en Cercle, Carre ou Triangle. Ceci parce que la seule chose que nous savons à ce moment là est que l'ArrayList est rempli de Formes. A la compilation, ceci est garanti uniquement par vos propres choix (ndt : le compilateur vous fait confiance), tandis qu'à l'exécution le transtypage est effectivement vérifié.

Maintenant le polymorphisme s'applique et la méthode exacte qui a été appelée pour une Forme est déterminée  selon que la référence est de type Cercle, Carre ou Triangle. Et en général, c'est comme cela qu'il faut faire ; on veut que la plus grosse partie du code ignore autant que possible le type spécifique des objets, et manipule une représentation générale de cette famille d'objets (dans notre cas, Forme). Il en résulte un code plus facile à écrire, lire et maintenir, et vos conceptions seront plus faciles à implémenter, comprendre et modifier. Le polymorphisme est donc un but général en programmation orientée objet.

Mais que faire si vous avez un problème de programmation qui peut se résoudre facilement si vous connaissez le type exact de la référence générique que vous manipulez ? Par exemple, supposons que vous désiriez permettre à vos utilisateurs de colorier toutes les formes d'un type particulier en violet. De cette manière, ils peuvent retrouver tous les triangles à l'écran en les coloriant. C'est ce que fait le RTTI : on peut demander à une référence sur une Forme le type exact de l'objet référencé.

L'objet Class

Pour comprendre comment marche le RTTI en Java, il faut d'abord savoir comment est représentée l'information sur le type durant l'exécution. C'est le rôle d'un objet spécifique appelé l'objet Class, qui contient toutes les informations relative à la classe (on l'appelle parfois meta-class). En fait, l'objet Class est utilisé pour créer tous les objets « habituels » d'une classe.

Il y a un objet Class pour chacune des classes d'un programme. Ainsi, à chaque fois qu'une classe est écrite et compilée, un unique objet de type Class est aussi créé (et rangé, le plus souvent, dans un fichier .class du même nom). Durant l'exécution, lorsqu'un nouvel objet de cette classe doit être créé, la MachineVirtuelle Java (Java Virtual Machine, JVM) qui exécute le programme vérifie d'abord si l'objet Class associé est déjà chargé. Si non, la JVM le charge en cherchant un fichier .class du même nom. Ainsi un programme Java n'est pas totalement chargé en mémoire lorsqu'il démarre, contrairement à beaucoup de langages classiques.

Une fois que l'objet Class est en mémoire, il est utilisé pour créer tous les objets de ce type.

Si cela ne vous semble pas clair ou si vous ne le croyez pas, voici un programme pour le prouver :

//: c12:Confiseur.java
// Étude du fonctionnement du chargeur de classes.

class Bonbon {
  static {
    System.out.println("Charge Bonbon");
  }
}

class Gomme {
  static {
    System.out.println("Charge Gomme");
  }
}

class Biscuit {
  static {
    System.out.println("Charge Biscuit");
  }
}

public class Confiseur {
  public static void main(String[] args) {
    System.out.println("Début méthode main");
    new Bonbon();
    System.out.println("Après création Gomme");
    try {
      Class.forName("Gomme");
    } catch(ClassNotFoundException e) {
      e.printStackTrace(System.err);
    }
    System.out.println(
      "Après Class.forName(\"Gomme\")");
    new Biscuit();
    System.out.println("Après création Biscuit");
  }
} ///:~

Chacune des classes Bonbon, Gomme et Biscuit a une clause static qui est exécutée lorsque la classe est chargée la première fois. L'information qui est affichée vous permet de savoir quand cette classe est chargée. Dans la méthode main(), la création des objets est dispersée entre des opérations d'affichages pour faciliter la détection du moment du chargement.

Une ligne particulièrement intéressante est :

Class.forName("Gomme");

Cette méthode est une méthode static de Class (qui appartient à tous les objets Class). Un objet Class est comme tous les autres objets, il est donc possible d'obtenir sa référence et de la manipuler (c'est ce que fait le chargeur de classes). Un des moyens d'obtenir une référence sur un objet Class est la méthode forName(), qui prend en paramètre une chaîne de caractères contenant le nom (attention à l'orthographe et aux majuscules !) de la classe dont vous voulez la référence. Elle retourne une référence sur un objet Class.

Le résultat de ce programme pour une JVM est :

Début méthode main
Charge Bonbon
Après création Bonbon
Charge Gomme
Après Class.forName("Gomme")
Charge Biscuit
Après création Biscuit

On peut noter que chaque objet Class est chargé uniquement lorsque c'est nécessaire, et que l'initialisation static est effectuée au chargement de la classe.

Les littéraux Class

Java fournit une deuxième manière d'obtenir une référence sur un objet de type Class, en utilisant le littéral class. Dans le programme précédent, on aurait par exemple :

Gomme.class;

ce qui n'est pas seulement plus simple, mais aussi plus sûr puisque vérifié à la compilation. Comme elle ne nécessite pas d'appel à une méthode, elle est aussi plus efficace.

Les littéraux Class sont utilisables sur les classes habituelles ainsi que sur les interfaces, les tableaux et les types primitifs. De plus, il y a un attribut standard appelé TYPE qui existe pour chacune des classes englobant des types primitifs. L'attribut TYPE produit  une référence à l'objet Class associé au type primitif, tel que :

... est équivalent à ...
boolean.class Boolean.TYPE
char.class Character.TYPE
byte.class Byte.TYPE
short.class Short.TYPE
int.class Integer.TYPE
long.class Long.TYPE
float.class Float.TYPE
double.class Double.TYPE
void.class Void.TYPE

Ma préférence va à l'utilisation des « .class » si possible, car cela est plus consistant avec les classes habituelles.

Vérifier avant de transtyper

Jusqu'à présent, nous avons vu différentes utilisations  de RTTI dont :

  1. Le transtypage classique ; i.e. « (Forme) », qui utilise RTTI pour être sûr que le transtypage est correct et lancer une ClassCastException si un mauvais transtypage est effectué.
  2. L'objet Class qui représente le type d'un objet. L'objet Class peut être interrogé afin d'obtenir des informations utiles durant l'exécution.

En C++, le transtypage classique « (Forme) » n'effectue pas de RTTI. Il indique seulement au compilateur de traiter l'objet avec le nouveau type. En Java, qui effectue cette vérification de type, ce transtypage est souvent appelé “transtypage descendant sain”. La raison du terme « descendant » est liée à l'historique de la représentation des diagrammes de hiérarchie de classes. Si transtyper un Cercle en une Forme est un transtypage ascendant, alors transtyper une Forme en un Cercle est un transtypage descendant. Néanmoins, on sait que tout Cercle est aussi une Forme, et le compilateur nous laisse donc librement effectuer un transtypage descendant ; par contre toute Forme n'est pas nécessairement un Cercle, le compilateur ne permet donc pas de faire un transtypage descendant sans utiliser un transtypage explicite.

Il existe une troisième forme de RTTI en Java. C'est le mot clef instanceof qui vous indique si un objet est d'un type particulier. Il retourne un boolean afin d'être utilisé sous la forme d'une question, telle que :

if(x instanceof Chien)
  ((Chien)x).aboyer();

L'expression ci-dessus vérifie si un objet x appartient à la classe Chien avant de transtyper x en Chien. Il est important d'utiliser instanceof avant un transtypage descendant lorsque vous n'avez pas d'autres informations vous indiquant le type de l'objet, sinon vous risquez d'obtenir une ClassCastException.

Le plus souvent, vous rechercherez un type d'objets (les triangles à peindre en violet par exemple), mais vous pouvez aisément identifier tous les objets en utilisant instanceof. Supposons que vous ayez une famille de classes d'animaux de compagnie (Pet) :

//: c12:Pets.java
class Pet {}
class Chien extends Pet {}
class Carlin extends Chien {}
class Chat extends Pet {}
class Rongeur extends Pet {}
class Gerbil extends Rongeur {}
class Hamster extends Rongeur {}

class Counter { int i; } ///:~

La classe Counter est utilisée pour compter le nombre d'animaux de compagnie de chaque type. On peut le voir comme un objet Integer que l'on peut modifier.

En utilisant instanceof, tous les animaux peuvent être comptés :

//: c12:PetCount.java
// Utiliser instanceof.
import java.util.*;

public class PetCount {
  static String[] typenames = {
    "Pet", "Chien", "Carlin", "Chat",
    "Rongeur", "Gerbil", "Hamster",
  };
  // Les exceptions remontent jusqu'à la console :
  public static void main(String[] args)
  throws Exception {
    ArrayList pets = new ArrayList();
    try {
      Class[] petTypes = {
        Class.forName("Chien"),
        Class.forName("Carlin"),
        Class.forName("Chat"),
        Class.forName("Rongeur"),
        Class.forName("Gerbil"),
        Class.forName("Hamster"),
      };
      for(int i = 0; i < 15; i++)
        pets.add(
          petTypes[
            (int)(Math.random()*petTypes.length)]
            .newInstance());
    } catch(InstantiationException e) {
      System.err.println("Instantiation impossible");
      throw e;
    } catch(IllegalAccessException e) {
      System.err.println("Accès impossible");
      throw e;
    } catch(ClassNotFoundException e) {
      System.err.println("Classe non trouvée");
      throw e;
    }
    HashMap h = new HashMap();
    for(int i = 0; i < typenames.length; i++)
      h.put(typenames[i], new Counter());
    for(int i = 0; i < pets.size(); i++) {
      Object o = pets.get(i);
      if(o instanceof Pet)
        ((Counter)h.get("Pet")).i++;
      if(o instanceof Chien)
        ((Counter)h.get("Chien")).i++;
      if(o instanceof Carlin)
        ((Counter)h.get("Carlin")).i++;
      if(o instanceof Chat)
        ((Counter)h.get("Chat")).i++;
      if(o instanceof Rongeur)
        ((Counter)h.get("Rongeur")).i++;
      if(o instanceof Gerbil)
        ((Counter)h.get("Gerbil")).i++;
      if(o instanceof Hamster)
        ((Counter)h.get("Hamster")).i++;
    }
    for(int i = 0; i < pets.size(); i++)
      System.out.println(pets.get(i).getClass());
    for(int i = 0; i < typenames.length; i++)
      System.out.println(
        typenames[i] + " quantité : " +
        ((Counter)h.get(typenames[i])).i);
  }
} ///:~

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