Comprendre la Réflexion en Java
La réflexion est un mécanisme puissant du langage Java qui permet d'inspecter et de manipuler des classes, interfaces, constructeurs, méthodes et champs durant l'exécution du programme, sans connaître leur existence au moment de la compilation.
Principe Fondamental
En termes techniques, la réflexion permet d'obtenir des informations sur n'importe quelle classe et d'agir sur n'importe quel objet au moment de l'exécution. Concrètement, cela signifie que vous pouvez créer des objets et invoquer des méthodes sans connaître à l'avance les classes concernées.
Cette capacité prend tout son sens lorsqu'elle est combinée avec des fichiers de configuration : le programme lit les noms des classes et des méthodes depuis un fichier externe, ce qui permet de modifier le comprotement de l'application sans recompiler le code source.
Les Trois Voies d'Accès à l'Objet Class
L'objet Class représente les métadonnées d'une classe chargée en mémoire. Voici les trois méthodes pour l'obtenir :
// Méthode 1 : via Class.forName() - la plus utilisée pour le chargement dynamique
Class<?> classePersonne = Class.forName("com.monpackage.Personne");
// Méthode 2 : via le littéral .class
Class<?> classePersonne2 = Personne.class;
// Méthode 3 : via la méthode getClass() d'une instance
Personne p = new Personne();
Class<?> classePersonne3 = p.getClass();
// Vérification : ces trois références pointent vers le même objet unique en mémoire
System.out.println(classePersonne == classePersonne2); // true
System.out.println(classePersonne2 == classePersonne3); // true
Chaque fichier .class chargé en mémoire génère un unique objet Class, partagé par toutes les instances de cette classe.
Manipulation des Constructeurs
Récupération des Constructeurs
L'API de réflexion distingue les méthodes getXxx() (publiques uniquement) et getDeclaredXxx() (tous les modificateurs, y compris privés).
Class<?> clazz = Class.forName("com.monpackage.Personne");
// Récupérer uniquement les constructeurs publics
Constructor<?>[] constructeursPublics = clazz.getConstructors();
// Récupérer tous les constructeurs, y compris privés
Constructor<?>[] tousConstructeurs = clazz.getDeclaredConstructors();
// Récupérer un constructeur spécifique (public)
Constructor<?> constructeurDefaut = clazz.getConstructor();
// Récupérer un constructeur avec paramètres (privé)
Constructor<?> constructeurPrive = clazz.getDeclaredConstructor(String.class, int.class);
Instanciation via Réflexion
public class Personne {
private String nom;
private int age;
public Personne() {}
private Personne(String nom, int age) {
this.nom = nom;
this.age = age;
}
}
// Utilisation dans une classe de test
Class<?> clazz = Class.forName("com.monpackage.Personne");
// Création avec constructeur par défaut
Constructor<?> consDefaut = clazz.getConstructor();
Personne personne1 = (Personne) consDefaut.newInstance();
// Création avec constructeur privé (nécessite setAccessible)
Constructor<?> consPrive = clazz.getDeclaredConstructor(String.class, int.class);
consPrive.setAccessible(true); // Contournement de l'encapsulation
Personne personne2 = (Personne) consPrive.newInstance("Dupont", 30);
Gestion des Champs (Attributs)
Accès aux Variables Membres
Class<?> clazz = Class.forName("com.monpackage.Personne");
// Tous les champs publics
Field[] champsPublics = clazz.getFields();
// Tous les champs déclarés (y compris privés)
Field[] tousChamps = clazz.getDeclaredFields();
// Champ spécifique par son nom
Field champNom = clazz.getDeclaredField("nom");
Lecture et Modification des Valeurs
Personne p = new Personne("Martin", 25);
Class<?> clazz = p.getClass();
Field champNom = clazz.getDeclaredField("nom");
champNom.setAccessible(true);
// Lecture de la valeur
String valeurNom = (String) champNom.get(p);
System.out.println("Nom actuel : " + valeurNom);
// Modification de la valeur
champNom.set(p, "Bernard");
System.out.println("Nouveau nom : " + p.getNom());
Invocation des Méthodes
Récupération des Méthodes
Class<?> clazz = Class.forName("com.monpackage.Personne");
// Méthodes publiques (héritées comprises)
Method[] methodesPubliques = clazz.getMethods();
// Toutes les méthodes déclarées dans la classe
Method[] methodesDeclarees = clazz.getDeclaredMethods();
// Méthode spécifique
Method methodeSePresenter = clazz.getMethod("sePresenter");
Method methodePrivee = clazz.getDeclaredMethod("calculerAge", int.class);
Exécution Dynamique des Méthodes
public class Personne {
private String nom;
public String sePresenter() {
return "Je suis " + nom;
}
private void definirNom(String nouveauNom) {
this.nom = nouveauNom;
}
}
// Invocation
Personne p = new Personne();
p.setNom("Alice");
Method methode = clazz.getMethod("sePresenter");
String resultat = (String) methode.invoke(p);
System.out.println(resultat); // "Je suis Alice"
// Méthode privée
Method methodePrive = clazz.getDeclaredMethod("definirNom", String.class);
methodePrive.setAccessible(true);
methodePrive.invoke(p, "Bob");
Applications Pratiques
Contourner les Génériques
Les génériques Java sont effacés à la compilation. La réflexion permet de contourner cette vérification :
List<Integer> nombres = new ArrayList<>();
nombres.add(10);
// Impossible directement : nombres.add("texte");
// Contournement via réflexion
Class<?> classeListe = nombres.getClass();
Method methodeAdd = classeListe.getMethod("add", Object.class);
methodeAdd.invoke(nombres, "texte");
System.out.println(nombres); // [10, "texte"]
Création Dynamique via Configuration
Cette technique est largement utilisée dans les framewokrs pour instancier des classes définies dans des fichiers de configuration.
Fichier config.properties :
classe.cible=com.monpackage.Personne
methode.cible=sePresenter
Code Java :
Properties config = new Properties();
config.load(new FileInputStream("config.properties"));
String nomClasse = config.getProperty("classe.cible");
String nomMethode = config.getProperty("methode.cible");
Class<?> clazz = Class.forName(nomClasse);
Object instance = clazz.getDeclaredConstructor().newInstance();
Method methode = clazz.getMethod(nomMethode);
methode.invoke(instance);
Sérialisation Générique d'Objets
La réflexion permet de créer des utilitaires capables de sauvegarder l'état de n'importe quel objet :
public static void serialiserObjet(Object objet, String fichier) throws Exception {
Class<?> clazz = objet.getClass();
BufferedWriter writer = new BufferedWriter(new FileWriter(fichier));
for (Field champ : clazz.getDeclaredFields()) {
champ.setAccessible(true);
String nomChamp = champ.getName();
Object valeurChamp = champ.get(objet);
writer.write(nomChamp + " = " + valeurChamp);
writer.newLine();
}
writer.close();
}
// Utilisation
Personne p = new Personne("Sophie", 28);
serialiserObjet(p, "personne.txt");
Considérations et Limitations
L'utilisation de la réflexion présente des avantages certains : flexibilité maximale, découplage du code, et capacité à ignorer les modificateurs d'accès. Cependant, elle introduit des risques de sécurité et une légère dégradation des performances. Son usage reste donc principalement réservé aux bibliothèques et frameworks, où cette flexibilité est essentielle.
Les versions récentes du JDK ont renforcé la sécurité, rendant certaines opérations de réflexion sur les classes sensibles (comme String) plus difficiles, voire impossibles sans options spécifiques de la machine virtuelle.