Tutoriel sur la réflexion en Java. On verra comment récupérer les constructeurs, méthodes et champs d'une classe en utilisant la réflexion. On verra également comment éxecuter une méthode, constructeur quelconque d'une classe.
Bonjour, ça fait très longtemps que je n'ai pas posté sur ce site par manque de temps depuis le début de mon activité professionnelle, c'est qu'avant, quand j'étais étudiant, c'était beaucoup plus facile !!!
Pour reprendre en douceur, voici un petit tutoriel sur la réflexion en Java (rien à voir à l'activité de Réfléchir pour ceux qui ont déjà sortis l'aspirine !).
Pour un débutant en Java, cet article ne lui sera pas d'une grande utilité. Bien que simple, la réflexion peut paraître un peu superflue ou inutile mais avec un peu d'expérience dans des projets informatique d'envergure, la réflexion peut rendre bien des services.
Petite note sur le code présenté dans ce tutoriel : le code est prévu pour Java 6. Il est néanmoins très facile de le rendre compatible avec une version inférieure. En cas de difficulté, me poser la question.
D'après Wikipédia,
En programmation informatique, la réflexion est la capacité d'un programme à examiner, et éventuellement à modifier, ses structures internes de haut niveau (par exemple ses objets) lors de son exécution.
Pour être plus clair, la réflexion c'est donner la possibilité au programme de pouvoir accéder/modifier la structure d'un objet pendant l'exécution du programme. Par structure d'un objet, on entend tout ce qui est propriété, classe, fonction etc.. En général, dans des langages « classiques » (C ou C++ par exemples), la structure d'un objet est uniquement connue au moment de la compilation, c'est-à-dire qu'il n'est pas possible, à l'exécution, de connaître « ce qu'a dans le ventre » un type d'objet quelconque.
Java implémente la réflexion. Il donne cependant uniquement la possibilité d'accéder aux structures et non pas de les modifier. Ainsi, à l'exécution d'un programme Java, il est tout à fait possible de connaître précisément la structure de n'importe quel objet et de pouvoir interagir avec (c'est à dire par exemple exécuter n'importe qu'elle fonction de l'objet) mais on ne peut pas par exemple, ajouter une fonction ou un champ à l'objet.
La structure d'un objet en Java est caractérisée par une classe qui contient des constructeurs, des méthodes (ou fonctions) et enfin des champs. La réflexion en Java permet d'avoir au moment de l'exécution la définition de n'importe quel objet tel que l'on avait programmé.
Le package java.lang.reflect est consacré à la réflexion. Comme vu plus haut, la structure d'un objet Java est caractérisée par une classe qui contient des constructeurs, des méthodes (ou fonctions) et enfin des champs. Les caractéristiques de l'objet sont représentés de manière formelle par une classe du package de réflexion :
Classe du package | Représente |
Class | Une classe. |
Method | Une méthode d'une classe |
Constructor | Constructeur d'une classe |
Field | Un champ d'une classe |
Notons, pour éviter une remarque en commentaire, que Class ne fait pas parti du package java.lang.reflect mais de java.lang, Néanmoins Class participe bien au mécanisme de la réflexion.
J'ai limité volontairement la liste des classes du package. Le but du présent tutoriel est de donner une première approche de la réflexion. Je laisserai donc le soin au lecteur de s'informer plus en détail s'il a besoin d'approfondir un peu.
Qu'est ce que ces classes nous apprennent sur l'objet ? La réponse est simple : tout !! Par exemple, à l'aide de Method, on peut connaître le nom de la méthode, les paramètres qu'elle prend et enfin le type de la valeur de retour.
Je vous recommande vivement de faire un tour sur la page suivante, qui est malheureusement pour certains, en anglais : http://java.sun.com/javase/6/docs/api/java/lang/reflect/package-summary.html
Pour mieux comprendre, prenons la classe suivante :
public class Test1 { private int unChamp2; public Test()3 { } public int methode(String arg)4 { } }
La structure de l'objet Test est donc la suivante :
Pour accéder à toute la structure d'un objet, tout se fait à partir de Class.
Pour récupérer la classe d'une instance d'un objet, rien de plus simple, un simple getClass sur l'objet et le tour est joué :
Class classe=obj.getClass();
Il est également possible de récupérer la classe à partir d'un type d'objet. Par exemple, pour String :
Class classe=String.class;
A partir de là, pour récupérer (des exemples sont développés plus bas) :
On a vu plus haut, que la réflexion ne servait pas seulement à accéder à la structure d'un objet mais aussi à interagir avec l'objet. Ainsi, on peut appeler une méthode quelconque, construire une instance à partir d'un constructeur ou affecter une valeur à un champ. Vous l'aurez compris, ces opérations sont réalisables à partir des classes Method, Constructor et Field.
Pour créer une nouvelle instance d'un objet, on utilise la fonction newInstance de Constructor. Cette fonction renvoie une nouvelle instance.
Ensuite, pour appeler une méthode, on utilise la fonction invoke de Method. Cette fonction permet d'exécuter la méthode sur une instance d'un objet.
Enfin, pour affecter une valeur à un champ, on utilise la fonction set de Field.
Nous allons mettre en pratique les notions vues jusqu'ici à travers plusieurs exemples pour mieux comprendre.
A travers les exemples ci-dessous, on va apprendre à utiliser la réflexion pour accéder à la structure d'un objet quelconque. Vous allez voir que c'est très simple.
Soit obj un objet quelconque, le code suivant donne sa Class. N'oublions pas que récupérer la classe est le point de départ pour avoir toutes les informations d'un objet
Class classe=obj.getClass();
Il est également possible de récupérer la classe d'un type par exemple de String :
Class classe=String.class ;
Du moment que l'on a récupéré la classe, on peut accéder aux méthodes de la classe.
Toutes les méthodes
Le code suivant donne toutes les méthodes de la classe dans un tableau.
import java.lang.reflect.Method; //ligne à rajouter en début de classe Method methods * =classe.getMethods();
Récupérer une méthode particulière
Une méthode est identifiée par sa signature c'est à dire son nom et la liste des types de paramètres. Notez que la réflexion Java permet de remonter uniquement les méthodes en visibilité public (contrairement au CSharp).
Imaginons que l'on veuille la fonction concat qui prend comme paramètre une chaîne de caractères (String). Le code est le suivant :
Method concat=classe.getMethod("concat", String.class);
Le premier paramètre de getMethod est le nom de la méthode recherchée. Attention le nom est sensible aux majuscules/minuscules. Les paramètres qui suivent sont les types des paramètres de la fonction. Ici, on veut la fonction concat avec exactement un paramètre de type String.
Si vous essayez ce code, votre compilateur risque de râler sur les exceptions non gérées.
Exceptions levées :
Quand on utilise getMethod, deux exceptions sont susceptibles d'êtres levées. Ces exceptions doivent obligatoirement être gérées pour que le code plus haut soit correct.
Ainsi, le code plus haut complet pour qu'il compile est le suivant :
try { Method concat=classe.getMethod("concat", String.class); } catch (SecurityException e) { //prb de sécurité e.printStackTrace(); } catch (NoSuchMethodException e) { //la méthode n'existe pas e.printStackTrace(); }
Un constructeur, contrairement à une méthode, n'est pas identifié par son nom (puisque par définition, un constructeur porte toujours le nom de la classe) mais uniquement par le type de ses paramètres. Comme pour les méthodes, notez que la réflexion Java permet de remonter uniquement les constructeurs en visibilité public.
Le principe reste le même que pour les méthodes sauf qu'au lieu d'utiliser getMethod, on utilise getConstructor.
Tous les constructeurs
import java.lang.reflect.Constructor; //à rajouter au début du fichier Constructor * constructeurs=classe.getConstructors();
Récupérer un constructeur en particulier
try { Constructor constructeur=classe.getConstructor(StringBuilder.class); } catch (SecurityException e) { //prb de sécurité e.printStackTrace(); } catch (NoSuchMethodException e) { //le constructeur n'existe pas e.printStackTrace(); }
Notez que les exceptions levées sont les mêmes que pour getMethod.
Pour récupérer un champ, on utilise getField. Il faut encore noter que l'on récupère uniquement les champs en visibilité public.
Tous les champs
import java.lang.reflect.Field; Field * champs=classe.getFields();
Un champ en particulier
Un champ est identifié uniquement par son nom. Supposons que l'on veuille le champ name de l'objet :
try { Field champ=classe.getField("name"); } catch (SecurityException e) { //prb de sécurité e.printStackTrace(); } catch (NoSuchFieldException e) { //le champ n'existe pas e.printStackTrace(); }
Quand le champ n'existe pas, c'est l'exception NoSuchFieldException qui est levée.
Avec les exemples ci-dessus, vous voyez que je ne vous avez pas menti : la réflexion est très simple.
C'est bien beau de récupérer les méthodes et ses informations, mais ça serait encore plus beau si on pouvait interagir avec n'importe quel objet ! Par exemple, appeler une méthode quelconque ou construire une instance quelconque.
Une fois qu'on a les Method, Constructor, et Field, il est encore plus simple d'effectuer des opérations sur un objet.
A partir de Constructor récupéré sur un type, il est très facile de construire une instance. On utilise pour cela la fonction newInstance. Cette fonction prend la liste des paramètres du constructeur.
En reprenant le constructeur récupéré plus haut, ce constructeur prend en paramètre un StringBuilder, il faut donc impérativement lui en passer un sous peine d'exception :
try { Object instance=constructeur.newInstance(new StringBuilder()); } catch (IllegalArgumentException e) { //le nombre d'arguments n'est pas bon e.printStackTrace(); } catch (InstantiationException e) { //la classe est abstraite (instance d'une classe abstraite impossible) e.printStackTrace(); } catch (IllegalAccessException e) { //le constructeur n'est pas accessible e.printStackTrace(); } catch (InvocationTargetException e) { //le constructeur a levé une exception e.printStackTrace(); }
La fonction newInstance va renvoyer la nouvelle instance du type.
Les exceptions qui peuvent être levées sont les suivantes :
A partir de l'objet Method, on peut exécuter la méthode sur une instance de l'objet. On utilise la fonction invoke qui prend en paramètre une instance d'un objet sur laquelle exécuter la méthode et la liste des paramètres de la méthode.
En reprenant la méthode récupérée dans l'exemple plus haut.
try { Object objet= " bonjour"); Object resultat=concat.invoke(objet, " tout le monde"); } catch (IllegalArgumentException e) { //les arguments passés à la méthode ne sont pas bon e.printStackTrace(); } catch (IllegalAccessException e) { //fonction pas accessible e.printStackTrace(); } catch (InvocationTargetException e) { //la méthode a levée une exception e.printStackTrace(); }
Si la méthode exécutée doit renvoyer une valeur, cette valeur est renvoyée en retour de l'appel à invoke.
Les exceptions qui peuvent être levées sont les suivantes :
Pour affecter une valeur à un champ d'une instance d'un objet, on utilise la méthode set de Field du champ à modifier.
try { champ.set(objet, "tuto"); } catch (IllegalArgumentException e) { //le parametre passé n'est pas valide (pas du bon type par exemple) e.printStackTrace(); } catch (IllegalAccessException e) { //fonction non accessible e.printStackTrace(); }
Les exceptions qui peuvent être levées sont les suivantes :
De la même manière, on peut lire la valeur d'un champ en utilisant la fonction get de Field.
try { Object lu=champ.get(objet); } catch (IllegalArgumentException e) { //le parametre passé n'est pas valide (pas du bon type par exemple) e.printStackTrace(); } catch (IllegalAccessException e) { //fonction non accessible e.printStackTrace(); }
Voici un exemple complet de réflexion. Etudiez ce code, il n'est vraiment pas compliqué, il regroupe toutes les notions vus dans ce tutoriel.
On défini 3 classes : A, B et C.
On va tirer au hasard une des trois classes. Sur cette classe, on affichera la liste des méthodes, des champs et des constructeurs en utilisant la réflexion.
Ensuite, toujours en utilisant la réflexion, une instance de la classe sera créée en utilisant un constructeur ayant un argument de type String s'il existe pour la classe.
On appellera ensuite la méthode, si elle existe, tutoriel sur l'instance qui vient d'être créé. Cette méthode doit prendre deux arguments de type String.
Pour finir, on affichera également les valeurs des champs de l'objet.
Voici le code complet, il doit être copié collé dans votre éditeur Java favori. L'exécution de l'exemple s'effectue en lançant simplement la classe ExempleComplet (contient une méthode main).
Fichier A.java
public class A { //**** Methodes **** private int champ1=45; public String champ2="valeur"; protected double champ3=2.0; public int valA=4; //*** Constructeurs **** public A() { } public A(double val,String val2) { } private A(int val,String val2) { } public A(String a) { } //*** Méthodes *** public int compute() { System.out.println("Compute éxecuté !!!"); return 0; } public void add(int a,int b) { System.out.println("add éxecuté !!!"); } private void div(int a,int b) { System.out.println("div éxecuté !!!"); } public String methodeA() { return "methodeA"; } }
Fichier B.java
public class B { //**** Methodes **** private String joueur="toto"; public int score=454676; //*** Constructeurs **** public B() { } public B(String joueur) { } //*** Méthodes *** public int tutoriel(String a,String b) { System.out.println("tutoriel de B éxecuté !!!"); return 0; } private int test() { System.out.println("test éxecuté !!!"); return 1; } public String methodeB() { return "methodeA"; } }
Fichier C.java
public class C { //**** Methodes **** private String unChamp="champ"; public int unChamp2=244; //*** Constructeurs **** public C() { } public C(String val) { } //*** Méthodes *** public int tutoriel(String a,String b) { System.out.println("tutoriel de C éxecuté !!!"); return 20; } public String toString() { System.out.println("toString éxecuté !!!"); return "Classe C"; } public String methodeC() { return "methodeA"; } }
Fichier ExempleComplet.java
import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class ExempleComplet { /** * Point d'entrée dans le prg * @param args */ public static void main(String * args) { //Tableau des classes Class classes * ={A.class,B.class,C.class}; //prenons une classe au hasard dans le tableau Class classe=classes[(int) (Math.random()*Integer.MAX_VALUE) % classes.length * ; //affiche a l'écran la classe choisie System.out.println("Classe choisie : "+classe.getName()); //affichons la liste des constructeurs de cette classe System.out.println("*** Liste des constructeurs de la classe ****"); Constructor constructeurs * =classe.getConstructors(); for(Constructor constructeur : constructeurs) { System.out.println("Constructeur : "+constructeur.getName()+", Nb d'arguments : "+constructeur.getParameterTypes().length); } //affichons la liste des méthodes de cette classe System.out.println("*** Liste des méthodes de la classe ****"); Method methodes * =classe.getMethods(); for(Method methode : methodes) { System.out.println("Méthode : "+methode.getName()+", Nb d'arguments : "+methode.getParameterTypes().length+", Type en retour : "+(methode.getReturnType()==null ? null : methode.getReturnType().getName())); } //affichons la liste des champs System.out.println("*** Liste des champs de la classe ****"); Field champs * =classe.getFields(); for(Field champ : champs) { System.out.println("Champ : "+champ.getName()+", Type : "+champ.getType().getName()); } //Récupere le constructeur ayant un parametre String System.out.println("*** Recupération du constructeur avec un parametre String ****"); Constructor monConstructeur=null; try { monConstructeur=classe.getConstructor(String.class); } catch (SecurityException e) { System.out.println("Erreur de sécurité sur le constructeur"); return; } catch (NoSuchMethodException e) { System.out.println("Le constructeur n'existe pas !"); return; } //construction d'une instance de l'objet System.out.println("*** Création d'une instance à partir de ce constructeur ****"); Object instance=null; try { instance=monConstructeur.newInstance("tuto"); } catch (IllegalArgumentException e) { System.out.println("Erreur Argument invalide"); return; } catch (InstantiationException e) { System.out.println("Erreur impossible d'instancier une classe abstraite"); return; } catch (IllegalAccessException e) { System.out.println("Erreur constructeur par accessible"); return; } catch (InvocationTargetException e) { System.out.println("Erreur exception levée par le constructeur"); return; } //Récupération de la méthode tutoriel avec deux arguments String System.out.println("*** Récupération de la méthode tutoriel ****"); Method tutoriel=null; try { tutoriel=classe.getMethod("tutoriel", String.class,String.class); } catch (SecurityException e) { System.out.println("Erreur de sécurité sur le constructeur"); return; } catch (NoSuchMethodException e) { System.out.println("Erreur la méthode n'existe pas"); return; } //Execution de la méthode tutoriel System.out.println("*** Execution de la méthode tutoriel ****"); try { Object retour=tutoriel.invoke(instance, "un","deux"); System.out.println("Valeur en retour="+(retour == null ? null : retour.toString())); } catch (IllegalArgumentException e) { System.out.println("Erreur argument invalide"); return; } catch (IllegalAccessException e) { System.out.println("Erreur la méthode n'est pas accessible"); return; } catch (InvocationTargetException e) { System.out.println("Erreur la méthode a provoquée une exception"); return; } //Affichage des valeurs des champs System.out.println("*** Liste des valeurs des champs ****"); for(Field champ : champs) { try { Object valeur=champ.get(instance); System.out.println("Valeur du champ "+champ.getName()+" : "+(valeur == null ? null : valeur.toString())); } catch (IllegalArgumentException e) { System.out.println("Erreur sur le champ "+champ.getName()+", argument non valide"); } catch (IllegalAccessException e) { System.out.println("Erreur sur le champ "+champ.getName()+" n'est pas accessible"); } } } }
Résultats d'exécutions
Quand la classe A est choisie :
-------------------------
Classe choisie : A
*** Liste des constructeurs de la classe ****
Constructeur : A, Nb d'arguments : 1
Constructeur : A, Nb d'arguments : 2
Constructeur : A, Nb d'arguments : 0
*** Liste des méthodes de la classe ****
Méthode : compute, Nb d'arguments : 0, Type en retour : int
Méthode : methodeA, Nb d'arguments : 0, Type en retour : java.lang.String
Méthode : add, Nb d'arguments : 2, Type en retour : void
Méthode : wait, Nb d'arguments : 0, Type en retour : void
Méthode : wait, Nb d'arguments : 2, Type en retour : void
Méthode : wait, Nb d'arguments : 1, Type en retour : void
Méthode : hashCode, Nb d'arguments : 0, Type en retour : int
Méthode : getClass, Nb d'arguments : 0, Type en retour : java.lang.Class
Méthode : equals, Nb d'arguments : 1, Type en retour : boolean
Méthode : toString, Nb d'arguments : 0, Type en retour : java.lang.String
Méthode : notify, Nb d'arguments : 0, Type en retour : void
Méthode : notifyAll, Nb d'arguments : 0, Type en retour : void
*** Liste des champs de la classe ****
Champ : champ2, Type : java.lang.String
Champ : valA, Type : int
*** Recupération du constructeur avec un parametre String ****
*** Création d'une instance à partir de ce constructeur ****
*** Récupération de la méthode tutoriel ****
Erreur la méthode n'existe pas
-------------------------
Si la liste des méthodes affichée vous parait suspecte, n'oubliez pas qu'en Java tout objet hérite de Object donc aussi de ses méthodes ! (méthodes wait, hashCode, getClass, equals, toString, notify et notifyAll).
Quand la classe B est choisie :
-------------------------
Classe choisie : B
*** Liste des constructeurs de la classe ****
Constructeur : B, Nb d'arguments : 0
Constructeur : B, Nb d'arguments : 1
*** Liste des méthodes de la classe ****
Méthode : tutoriel, Nb d'arguments : 2, Type en retour : int
Méthode : methodeB, Nb d'arguments : 0, Type en retour : java.lang.String
Méthode : wait, Nb d'arguments : 0, Type en retour : void
Méthode : wait, Nb d'arguments : 2, Type en retour : void
Méthode : wait, Nb d'arguments : 1, Type en retour : void
Méthode : hashCode, Nb d'arguments : 0, Type en retour : int
Méthode : getClass, Nb d'arguments : 0, Type en retour : java.lang.Class
Méthode : equals, Nb d'arguments : 1, Type en retour : boolean
Méthode : toString, Nb d'arguments : 0, Type en retour : java.lang.String
Méthode : notify, Nb d'arguments : 0, Type en retour : void
Méthode : notifyAll, Nb d'arguments : 0, Type en retour : void
*** Liste des champs de la classe ****
Champ : score, Type : int
*** Recupération du constructeur avec un parametre String ****
*** Création d'une instance à partir de ce constructeur ****
*** Récupération de la méthode tutoriel ****
*** Execution de la méthode tutoriel ****
tutoriel de B éxecuté !!!
Valeur en retour=0
*** Liste des valeurs des champs ****
Valeur du champ score : 454676
-------------------------
Quand la classe C est choisie :
-------------------------
Classe choisie : C
*** Liste des constructeurs de la classe ****
Constructeur : C, Nb d'arguments : 0
Constructeur : C, Nb d'arguments : 1
*** Liste des méthodes de la classe ****
Méthode : tutoriel, Nb d'arguments : 2, Type en retour : int
Méthode : methodeC, Nb d'arguments : 0, Type en retour : java.lang.String
Méthode : toString, Nb d'arguments : 0, Type en retour : java.lang.String
Méthode : wait, Nb d'arguments : 0, Type en retour : void
Méthode : wait, Nb d'arguments : 2, Type en retour : void
Méthode : wait, Nb d'arguments : 1, Type en retour : void
Méthode : hashCode, Nb d'arguments : 0, Type en retour : int
Méthode : getClass, Nb d'arguments : 0, Type en retour : java.lang.Class
Méthode : equals, Nb d'arguments : 1, Type en retour : boolean
Méthode : notify, Nb d'arguments : 0, Type en retour : void
Méthode : notifyAll, Nb d'arguments : 0, Type en retour : void
*** Liste des champs de la classe ****
Champ : unChamp2, Type : int
*** Recupération du constructeur avec un parametre String ****
*** Création d'une instance à partir de ce constructeur ****
*** Récupération de la méthode tutoriel ****
*** Execution de la méthode tutoriel ****
tutoriel de C éxecuté !!!
Valeur en retour=20
*** Liste des valeurs des champs ****
Valeur du champ unChamp2 : 244
-------------------------
Pour conclure, comme vous pouvez le constater, la réflexion est très simple et ouvre de nombreuses possibilités. Néanmoins, sous prétexte que vous avez appris un nouveau concept, ce n'est pas une raison pour l'utiliser à toutes les sauces ! Il est parfois bien plus simple d'utiliser des interfaces ou des classes abstraites.
Un des inconvénients de la réflexion, c'est le manque de lisibilité du code. La réflexion est utile pour effectuer des traitements automatiques sur le contenu des objets. Personnellement, dans un projet de site web, pour afficher toutes les informations d'un objet dans une page automatiquement (c'est-à-dire sans se farcir un à un les accès à tous les champs de l'objet), l'utilisation de la réflexion a été pratique et m'a fait gagné un temps fou. En effet, dans chaque page, en parcourant le contenu d'un objet par réflexion, en quelques lignes, j'assure qu'a la moindre modification structurelle de l'objet (par exemple on ajoute un champ dans celui ci) que les répercutions soient faites automatiquement sans effort dans toutes les pages affichant cet objet.
Sur ce, je vous souhaite une bonne Java !! (ah ah c'est fou l'humour que j'ai ! )