Introspection et réflexivité en Java

extrait de http://ricky81.developpez.com/tutoriel/java/api/reflection/ riky81

Présentation du paquetage java.lang.reflect

Le paquetage java.lang.reflect contient les éléments indispensables pour utiliser l'introspection. Mais commençons par évoquer la classe java.lang.Class qui est la classe centrale sur laquelle repose la réflexivité.


 La classe Class

En effet, vous avez sans doute déjà rencontré cette classe à l'occasion d'un :
Class.forName(maClasse);
qui permet de récupérer un objet de type Class correspondant au nom passé en paramètre (par exemple à l'occasion de l'utilisation de JDBC).

En fait, les instances de la classe Class peuvent être des classes ou des interfaces. Cette classe est indispensable pour pouvoir manipuler des méta données. Nous verrons que cette classe sera présente dans chaque exemple que nous rencontrerons, dont en voici un premier :
public class Exemple
{
   public Exemple()
   {
   }

   public String getNom(Object o)
   {
      Class c = o.getClass();
      return c.getName();
   }	
}
Dans cet exemple, la méthode getNom va renvoyer le nom de la classe de l'objet passé en paramètre.

Cette classe dispose des méthodes permettant d'extraire les informations sur une classe quelconque. En voici quelques unes :

java.lang.reflect.Field getField(String name)

Renvoie un objet Field correspondant au champ "name"
java.lang.reflect.Field[] getFields()

Renvoie l'ensemble des champs publics sous la forme d'un tableau
java.lang.reflect.Method getMethod(String name, Class[] parameterTypes)

Renvoie un objet Method correspondant à la méthode "name" avec les paramètres définis par le tableau parameterTypes
java.lang.reflect.Method[] getMethods()

Renvoie l'ensemble des méthodes publiques sous la forme d'un tableau
java.lang.reflect.Constructor getConstructor(Class[] parameterTypes)

Renvoie un objet Constructor correspondant au constructeur avec les paramètres defines par le tableau parameterTypes
java.lang.reflect.Constructor[] getConstructors()

Renvoie l'ensemble des constructeurs sous la forme d'un tableau
Class[] getInterfaces()

Renvoie l'ensemble des interfaces implémentées
Class getSuperclass()

Renvoie la classe mère
java.lang.Package getPackage()

Renvoie un objet Package correspondant au paquetage dans lequel se trouve la classe


La classe Field

Venons en à notre paquetage java.lang.reflect.
Nous avons vu qu'il était possible de récupérer des méta données sur les champs à partir d'une classe en appelant par exemple la méthode getField.
Partant d'un tel objet, nous pouvons consulter les informations à l'aide de méthodes de classe :

String getName()

Renvoie le nom du champ
Class getType()

Renvoie le type du champ
int getModifier()

Renvoie un entier codant la visibilité du champ (private, protected, public) mais également d'autres informations comme static

 La classe Modifier

Nous avons vu précédemment qu'il était possible de connaître la visibilité d'un champ par l'intermédiaire de la méthode getModifier().
Il peut être surprenant de constater qu'il existe une classe Modifier alors que la méthode précédente renvoie un entier. Ce qu'il faut bien avoir à l'esprit pour comprendre ceci, c'est qu'il y a plusieurs informations à l'intérieur de cet entier, et il est nécessaire de faire appel à des méthodes de classe pour les distiller (néanmoins on conviendra qu'il pourrait être plus logique d'adapter la classe Modifier pour pouvoir faire renvoyer un objet plutôt qu'un entier).

Voici quelques unes des méthodes utiles :

boolean static isFinal(int mod)

Détermine si le code transmis intègre la particularité "final"
boolean static isPublic(int mod)

Détermine si le code transmis intègre une visibilité publique
boolean static isStatic(int mod)

Détermine si le code transmis intègre la particularité "static"
Ainsi, le code suivant permet de déterminer si le champ 'f' possède une visibilité publique :
if(java.lang.reflect.Modifier.isPublic(f.getModifier()))
{
   System.out.println("champ à visibilité publique");
}
Néanmoins, il existe une méthode assez pratique, à savoir String static toString(int mod), qui permet de récupérer une description classique complète des informations.


La classe Method

Pourquoi ne pas avoir géré les informations disponibles dans la classe Modifier directement dans la classe Field me direz vous ? De telles informations sont justement également disponibles pour les méthodes et il est donc plus censé de les factoriser dans une classe spécifique.

Pour en revenir à nos méthodes, nous avons vu qu'il était possible de récupérer un objet décrivant une méthode, et nous allons voir ce qu'il est possible d'en extraire comme informations :

Class[] getExceptionTypes()

Renvoie un tableau contenant les classes Exception déclarées comme pouvant être lancées
String getName()

Renvoie le nom de la méthode
Class getReturnType()

Renvoie la classe du paramètre retourné par la méthode
Class[] getParameterTypes()

Renvoie un tableau contenant les classes des paramètres de la méthode
int getModifiers()

Renvoie un entier codant la visibilité de la méthode (private, protected, public) mais également d'autres informations comme static

 La classe Constructor

Enfin, dans le même esprit que pour la classe Method, on retrouve un classe Constructor qui permet de récupérer des informations sur un constructeur.

Class[] getExceptionTypes()

Renvoie un tableau contenant les classes Exception déclarées comme pouvant être lancées
String getName()

Renvoie le nom de la méthode
Class[] getParameterTypes()

Renvoie un tableau contenant les classes des paramètres de la méthode
int getModifiers()

Renvoie un entier codant la visibilité de la méthode (private, protected, public) mais également d'autres informations comme static

Cas particulier des types primitifs

Le lecteur averti aura sans doute une question concernant la compatibilité entre la classe Class et les types primitifs de Java comme int, long, char, ...
Ces types n'étant pas des objets, comment se passe la compatibilité avec les méthodes que nous avons vues précédemment et qui renvoient des objets de type Class ? Et bien ce sont les types objets enveloppe correspondants (Integer, Long, Char, ...) qui seront renvoyés.
Même remarque lorsque nous tenterons d'appeler une méthode dynamiquement à l'aide de la réflexivité : il faudra passer comme paramètre un objet et non le type primitif qui est utilisé au niveau de la méthode, mais également caster le résultat renvoyé dans le type objet enveloppe correspondant (cela ne devrait plus être nécessaire avec Java 1.5).

Néanmoins, se pose alors une autre question. A savoir : comment distinguer 2 méthodes entièrement identiques ne différant que par un type lequel est pour l'une un type primitif, et pour l'autre le type objet enveloppe correspondant ?
Prenons l'exemple du couple Integer/int : pour le premier ce sera Integer.class et pour le second Integer.TYPE. Ainsi, dans l'exemple suivant :
Class c = Class.forName("maClasse");
Class[] p1 = new Class[]{Integer.class, Integer.class);
Class[] p2 = new Class[]{Integer.TYPE, Integer.TYPE);
Method m1 = c.getMethod("somme",p1);
Method m2 = c.getMethod("somme",p2);
m1 correspond ici à la méthode somme(Integer, Integer) alors que m2 correspond à somme(int, int).


Utilisation de la réflexivité - Exemples d'appels dynamiques

Voyons à présent quelques exemples d'utilisation pratique de la réflexivité.
Nous allons dans un premier temps essayer de modifier le contenu du champ d'un objet de façon dynamique en prenant pour paramètres un objet, le nom du champ qu'on souhaite modifier, et la nouvelle valeur. Puis, nous travaillerons au niveau des méthodes pour vous montrer ce qu'il est possible de réaliser.


 Edition d'un champ

Imaginez le problème suivant : vous souhaitez pouvoir modifier la valeur du champ d'un objet de façon dynamique. Vous allez donc passer en paramètre le nom de ce champ sous la forme d'un String, mais vous ne pourrez pas agir sur ce champ avec les moyens classiques. Considérons donc une telle méthode :
void changeValeur(Object o, String nomChamp, Object val)
Il n'est bien entendu pas possible d'accéder au contenu du champ comme en programmation statique par :
o.nomChamp;
Nous sommes donc obligés de faire appel à la réflexivité.
Voici le code que nous allons par la suite commenter :
void changeValeur(Object o, String nomChamp, Object val) throws Exception
{
   Field f = o.getClass().getField(nomChamp);
   f.set(o,val);
}
Nous récupérons donc un objet de type Field correspondant au champ concerné par la modification, puis nous faisons appel à la méthode set sur ce champ qui permet de modifier le contenu du champ d'un objet (ici o) en lui attribuant la valeur passée en second paramètre.
La méthode set est la méthode la plus générale pour faire une affectation sur un objet, mais il existe des méthodes plus restreintes tel que setDouble(Object obj, double d)ou setBoolean(Object obj, boolean z).

Il y a bien entendu la possibilité d'utiliser des méthodes de consultation, ce qui permet d'écrire une méthode générique d'affichage du contenu d'un champ d'un objet :
void afficheValeur(Object o, String nomChamp) throws Exception
{
   Field f = o.getClass().getField(nomChamp);
   System.out.println(f.get(o));
}

Appel d'une méthode

Après avoir vu qu'il était possible d'agir sur les champs d'un objet à l'aide de la réflexivité, nous allons maintenant nous intéresser aux méthodes. Nous allons ainsi pouvoir lancer une méthode sur un objet de façon entièrement dynamique, en gérant bien entendu le passage des paramètres souhaités à la méthode.

Nous avons vu une certaine catégorie de méthodes pour la classe Method, à savoir ce qui concerne l'interrogation des méta données d'une méthode.
Comme nous avons vu comment récupérer la valeur d'un champ et la modifier, nous allons voir maintenant la méthode à utiliser pour lancer dynamiquement une méthode sur un objet ou une classe. La méthode en question est invoke dont voici la déclaration :
Object invoke(Object obj, Object[] args)
Dans le même esprit des exemples précédents, nous pouvons alors déclarer la méthode suivante :
Object lancerMethode(Object o, Object[] args, String nomMethode) throws Exception
{
  Class[] paramTypes = null;
  if(args != null)
  {
    paramTypes = new Class[args.length];
    for(int i=0;i<args.length;++i)
    {
      paramTypes[i] = args[i].getClass();
    }
  }
  Method m = o.getClass().getMethod(nomMethode,paramTypes);
  return m.invoke(o,args);
}
Cette méthode permet donc de lancer une méthode sur un objet, et éventuellement de récupérer un résultat, et ceci en récupérant la liste des paramètres et le nom de la méthode. Attention cependant : comme cela a été dit précédemment, la recherche de la méthode par getMethod va examiner les types des paramètres et il est donc indispensable de ne pas se tromper au niveau des paramètres. De plus, cela pose quelques problèmes pour les méthodes génériques ayant pour paramètre le type Objectmais qu'on désire appeler avec un paramètre d'un type dérivé. En effet, getMethod cherchera la méthode dont le paramètre a pour type le type dérivé en question, et comme il ne le trouvera pas il renverra une exception.
Je vous laisse chercher une solution à ce problème ...

Dans le cas d'une méthode de classe, le paramètre obj de la méthode invoke ne sera pas pris en compte. Vous pouvez tout aussi bien mettre null que n'importe quel objet.

Réalisation d'un inspecteur de classe

Voici un exemple très basique de ce qu'on peut faire pour consulter les informations à visibilité publique d'une classe en affichant les informations avec le classique System.out.
import java.lang.reflect.*;

public class Explorateur 
{
   public Explorateur()
   {
   }
  
   public void explorerChamps(Object o)
   {
      Field[] f = null;
      Class c = null;
    
      c = o.getClass();
      f = c.getFields();
      consulterChamps(f,o);
   }
  
   public void explorerMethodes(Object o)
   {
      Method[] m = null;
      Class c = null;
    
      c = o.getClass();
      m= c.getMethods();
      consulterMethodes(m);
   }
  
   private void consulterChamps(Field[] f, Object o)
   {
      for(int i=0;i<f.length;++i)
      {      
         System.out.print(Modifier.toString(f[i].getModifiers()));
         System.out.print(" ");
         System.out.print(f[i].getType().getName());
         System.out.print(" ");
         System.out.print(f[i].getName());
         System.out.print(" = ");
         try
         {
            System.out.println(f[i].get(o));
         }
         catch (IllegalAccessException e)
         {
           System.out.println("Valeur non consultable");
         }
      }
   }
  
   private void consulterMethodes(Method[] m)
   {
      Class[] params = null;
      for(int i=0;i<m.length;++i) 
      {      
         System.out.print(Modifier.toString(m[i].getModifiers()));
         System.out.print(" ");
         System.out.print(m[i].getReturnType().getName());
         System.out.print(" ");
         System.out.print(m[i].getName());
         System.out.print("(");
         params = m[i].getParameterTypes();
         for(int j=0;j<params.length;++j)
         {
           System.out.print(params[j].getName());
         }
         System.out.println(")");
      } 
   }
  
}

Comments