< Programmation Java

La réflexion (reflection en anglais) permet l'introspection des classes, c'est-à-dire de charger une classe, d'en créer une instance et d'accéder aux membres statiques ou non (appel de méthodes, lire et écrire les attributs) sans connaître la classe par avance.

Java possède une API permettant la réflexion. Elle sert notamment à gérer des extensions (plug-in en anglais) pour une application.

L'API de réflexion

Le package java.lang possède trois classes utilisées pour l'utilisation dynamique des classes :

java.lang.Class
Cette classe permet d'accéder aux caractéristiques d'une classe, à ses membres (méthodes et attributs), à la classe mère.
java.lang.ClassLoader
Cette classe permet de gérer le chargement de classe. Il existe des sous-classes dont notamment java.net.URLClassLoader permettant de charger des classes en les cherchant dans une liste d'URLs (donc de fichiers JAR et répertoires également, en convertissant le chemin du fichier ou répertoire en URL).
java.lang.Package
Cette classe permet d'accéder aux informations d'un package (informations de version, annotations, ...).

Les autres classes utiles sont définies dans le package java.lang.reflect et permettent d'accéder aux détails d'une classe. Les principales classes sont les suivantes :

java.lang.reflect.Constructor
Référence à un constructeur d'une classe.
java.lang.reflect.Method
Référence à une méthode d'une classe.
java.lang.reflect.Field
Référence à un champ d'une classe.
java.lang.reflect.Modifier
Attributs et méthodes statiques pour décoder les modificateurs des membres (public, private, protected, static, abstract, final, native, ...).

Les classes représentant des membres d'une classe (Constructor, Method, Field) implémentent toutes l'interface java.lang.reflect.Member comportant les méthodes suivantes :

Class getDeclaringClass()
Retourne la classe définissant ce membre.
String getName()
Retourne le nom.
int getModifiers()
Retourne les modificateurs (public, protected, private, static, final, ...).
boolean isSynthetic()
Teste si ce membre a été généré par le compilateur.

Charger une classe dynamiquement

La classe java.lang.Class possède deux méthodes statiques pour obtenir une classe (après chargement si nécessaire) :

static Class forName(String name)
Cette méthode équivaut à appeler la seconde méthode avec pour paramètres : (name, true, this.getClass().getClassLoader()).
static Class forName(String name, boolean initialize, ClassLoader loader)
Charge la classe dont le nom complet (incluant les packages) est spécifié, en utilisant l'instance du chargeur de classe fourni. Le paramètre initialize vaut true pour initialiser la classe (appeler le bloc d'initialisation statique), ou false pour ne pas l'initialiser.

Il est également possible d'obtenir une java.lang.Class de manière statique :

  • à partir d'un objet en appelant la méthode getClass(),
  • à partir d'une référence à la classe en utilisant le champ class.

Exemple :

package org.wikibooks.fr;

public class Exemple
{
    String nom;
    public String getNom()
    { return nom; }
}
...

Class c = Class.forName("org.wikibooks.fr.Exemple"); // sans référence statique à la classe
// ou
Class c = org.wikibooks.fr.Exemple.class; // référence statique à la classe
// ou
Class c = new Exemple().getClass(); // référence statique à la classe

Liste des membres d'une classe

Les méthodes suivantes permettent de lister les membres d'une classe :

getConstructors()
Cette méthode retourne un tableau de java.lang.reflect.Constructor contenant tous les constructeurs définis par la classe.
getMethods()
Cette méthode retourne un tableau de java.lang.reflect.Method contenant toutes les méthodes définies par la classe.
getFields()
Cette méthode retourne un tableau de java.lang.reflect.Field contenant tous les attributs définis dans la classe.

Les méthodes ci-dessus retournent les membres publics de la classe, comprenant également ceux hérités des classes mères. Il existe une variante "Declared" de ces méthodes retournant tous les membres (publics, protégés, privés) déclarés par la classe uniquement (les membres hérités sont exclus).

Au lieu de lister tous les membres, puis en rechercher un en particulier, il est possible d'utiliser les méthodes spécifiques de recherche d'un membre précis d'une classe (publics et hérités, ou bien "Declared" pour tous ceux déclarés par la classe seule) :

getConstructor(Class... parameterTypes)
getDeclaredConstructor(Class... parameterTypes)
Cette méthode retourne le constructeur déclaré avec les paramètres dont les types sont spécifiés.
getMethod(String name, Class... parameterTypes)
getDeclaredMethod(String name, Class... parameterTypes)
Cette méthode retourne la méthode portant de nom spécifié et déclarée avec les paramètres dont les types sont spécifiés.
getField(String name)
getDeclaredField(String name)
Cette méthode retourne l'attribut portant de nom spécifié.

Les membres retournés par toutes ces méthodes peuvent être d'instance ou statiques.

La méthode getAnnotations() retourne un tableau de java.lang.Annotation contenant toutes les annotations associées à la classe.

Instancier une classe et appel à un constructeur

La méthode newInstance() de la classe java.lang.Class permet de créer une nouvelle instance de la classe, en appelant le constructeur sans paramètre de la classe (qui doit donc en posséder un) :

Class c = Class.forName("org.wikibooks.fr.Exemple");

Object o = c.newInstance();  // équivaut à   new org.wikibooks.fr.Exemple();

Une classe comme celle ci-dessous peut ne pas avoir de constructeur sans paramètres :

package org.wikibooks.fr;

public class Livre
{
    String titre;
    int nb_pages;

    public Livre(String titre, int nb_pages)
    {
        this.titre = titre;
        this.nb_pages = nb_pages;
    }
}

Dans ce cas, il faut d'abord obtenir le constructeur, puis l'appeler :

Class c = Class.forName("org.wikibooks.fr.Livre"); // Accès à la classe Livre
Constructor constr = c.getConstructor(String.class, int.class); // Obtenir le constructeur (String, int)
Object o = constr.newInstance("Programmation Java", 120); // -> new Livre("Programmation Java", 120);

Pour les versions de Java antérieures à 5.0 où l'auto-boxing n'existe pas, et où il faut explicitement utiliser des tableaux :

Class c = Class.forName("org.wikibooks.fr.Livre"); // Accès à la classe Livre
Constructor constr = c.getConstructor(new Class[]{ String.class, Integer.TYPE }); // Obtenir le constructeur (String, int)
Object o = constr.newInstance(new Object{ "Programmation Java", Integer.valueOf(120) }); // -> new Livre("Programmation Java", 120);

Appel à une méthode

L'appel à une méthode de la classe est basé sur le même principe que l'appel à un constructeur vu juste avant. Cependant, pour obtenir la référence à une méthode, il faut spécifier le nom. Lors de l'invocation de la méthode, il faut spécifier l'instance (l'objet) auquel s'applique la méthode (null pour une méthode statique).

Exemple :

package org.wikibooks.fr;

public class Livre
{
    String titre;
    int nb_pages;
  
    public Livre(String _titre, int _nb_pages) {  
        this.titre = _titre;
        this.nb_pages = _nb_pages;
    }

    public int getNombreDeFeuilles(int pages_par_feuille)
    {
        return (nb_pages+pages_par_feuille-1)/pages_par_feuille;
    }
}
...
Class c = Class.forName("org.wikibooks.fr.Livre"); // Accès à la classe Livre

Constructor constr = c.getConstructor(String.class, int.class); // Obtenir le constructeur (String, int)
Object o = constr.newInstance("Programmation Java", 120); // -> new Livre("Programmation Java", 120);

Method method = c.getMethod("getNombreDeFeuilles", int.class); // Obtenir la méthode getNombreDeFeuilles(int)
int nb_feuilles = (int)method.invoke(o, 2); // -> o.getNombreDeFeuilles(2);

Accès à un attribut public

L'accès à un attribut public se fait en appelant les méthodes sur l'instance de java.lang.reflect.Field obtenu auprès de la classe.

Exemple :

package org.wikibooks.fr;

public class Livre
{
    public String titre;
    public int nb_pages;

    public Livre(String _titre, int _nb_pages) {  
        this.titre = _titre;
        this.nb_pages = _nb_pages;
    }
}
...
Class c = Class.forName("org.wikibooks.fr.Livre"); // Accès à la classe Livre

Constructor constr = c.getConstructor(String.class, int.class); // Obtenir le constructeur (String, int)
Object o = constr.newInstance("Programmation Java", 120); // -> new Livre("Programmation Java", 120);

Field f_titre = c.getField("titre"); // Obtenir l'attribut titre
String titre_du_livre = (String)f_titre.get(o); // -> o.titre
f_titre.set(o, "Java"); // -> o.titre = "Java";

Accès à un attribut privé

Il est également possible d'avoir accès aux champs privés grâce à la méthode setAccessible de la classe Field. Cela est cependant fortement déconseillé puisque modifier les valeurs d'un champs privée revient à violer le principe d'encapsulation.

Exemple :

package org.wikibooks.fr;

public class Livre
{
    private String titre;
    private int nb_pages;

    public Livre(String _titre, int _nb_pages) {  
        this.titre = _titre;
        this.nb_pages = _nb_pages;
    }
}
...
Class c = Class.forName("org.wikibooks.fr.Livre"); // Accès à la classe Livre

Constructor constr = c.getConstructor(String.class, int.class); // Obtenir le constructeur (String, int)
Object o = constr.newInstance("Programmation Java", 120); // -> new Livre("Programmation Java", 120);

Field f_titre = c.getField("titre"); // Erreur: titre est privé

Field fields[] = c.getDeclaredFields();
fields[0].setAccessible(true); // titre désormais équivalent à un attribut publique
Field f_titre = fields[0]; // Obtenir l'attribut titre

String titre_du_livre = (String)f_titre.get(o); // -> o.titre
f_titre.set(o, "Java"); // -> o.titre = "Java";

Exemple concret : un gestionnaire d'extensions

L'extension d'une application Java peut se faire en utilisant la réflexion pour charger dynamiquement une classe. Le but de cet exemple est de permettre d'ajouter des fonctionnalités dynamiquement à une application, sans avoir à effectuer de changement autre que l'ajout d'une nouvelle archive JAR dans un répertoire.

Par défaut, une application est composée de plusieurs classes dont le chargement est effectué par la JVM. Cependant, il faut que les chemins des répertoires de fichiers *.class ou des archives *.jar soit renseignés d'avance avant le lancement de l'application dans le classpath.

Cet article est issu de Wikibooks. Le texte est sous licence Creative Commons - Attribution - Partage dans les Mêmes. Des conditions supplémentaires peuvent s'appliquer aux fichiers multimédias.