Java Reflection : Inspection des champs
Inspectez, lisez et modifiez des champs à l'exécution en Java avec l'API de reflection.
Un objet Field décrit un champ d'une classe : son nom, son type, ses modificateurs, et — pour une instance donnée — sa valeur. La reflection permet de lister les champs d'une classe, de les lire et de les écrire, même lorsqu'ils sont private et n'ont ni getter ni setter. C'est exactement ainsi que les désérialiseurs JSON peuplent les objets et que les ORM hydratent les entités. Ce chapitre traite de l'obtention d'objets Field, de la lecture et de l'écriture de valeurs, de la porte setAccessible, et du cas particulier des champs final.
Ce chapitre s'appuie sur l'introduction à la reflection. Pour les API associées, consultez l'inspection des méthodes et l'inspection des constructeurs.
Obtenir des objets Field
La même distinction public-vs-déclaré vue dans l'introduction s'applique ici :
Class<?> c = User.class;
Field f1 = c.getField("name"); // public field, incl. inherited — else NoSuchFieldException
Field f2 = c.getDeclaredField("name"); // any access level, this class only
Field[] all = c.getFields(); // public fields, incl. inherited
Field[] mine = c.getDeclaredFields(); // all access levels, this class onlygetField/getFields ne voient que les champs public mais suivent la chaîne d'héritage. getDeclaredField/getDeclaredFields voient aussi les champs private/protected/package, mais uniquement ceux déclarés littéralement sur la classe demandée. Pour collecter tous les champs y compris les champs privés hérités, il faut parcourir getSuperclass() et les fusionner.
Métadonnées d'un champ : nom, type, modificateurs, génériques
Un Field répond à des questions sur lui-même sans nécessiter d'instance :
Field f = User.class.getDeclaredField("age");
f.getName(); // "age"
f.getType(); // int.class — the erased type
f.getGenericType(); // int — Type, keeps generic info
f.getModifiers(); // int bitset
Modifier.isPrivate(f.getModifiers()); // true/false
Modifier.isStatic(f.getModifiers());
Modifier.isFinal(f.getModifiers());
f.getDeclaringClass(); // class …UsergetType() retourne la Class effacée (List) ; getGenericType() retourne un Type qui, pour un champ List<String>, peut être casté en ParameterizedType pour récupérer String. Cette récupération fonctionne car les signatures génériques des champs sont conservées dans le fichier de classe même si les instances sont effacées.
Lire et écrire des valeurs
Pour lire ou écrire, vous avez besoin d'une instance (ou null pour un champ static) et vous devez passer le contrôle d'accès :
User u = new User("ada", 36);
Field age = User.class.getDeclaredField("age");
age.setAccessible(true); // bypass the access check for private
int current = age.getInt(u); // typed getter for primitives → 36
age.setInt(u, 37); // typed setter
Object boxed = age.get(u); // generic getter, autoboxes → Integer 37
age.set(u, 40); // generic setter, autounboxesIl existe des accesseurs typés — getInt, getBoolean, getDouble, setLong, … — pour les champs primitifs, et les méthodes génériques get(Object)/set(Object,Object) pour tout champ (en autoboxant les primitives). Pour un champ static, passez null comme cible : staticField.get(null).
La porte setAccessible
Par défaut, un Field applique les règles d'accès Java : la lecture reflective d'un champ private lève une IllegalAccessException. field.setAccessible(true) supprime cette vérification pour cet objet Field. C'est ce qui permet à la reflection d'accéder aux éléments internes — et ce qui la rend dangereuse.
Deux mises en garde depuis Java 9 :
- Frontières de modules. Si le type cible se trouve dans un module qui n'a pas
opensson paquet vers vous,setAccessible(true)lève uneInaccessibleObjectException. Les bibliothèques vous demandent d'ajouter--add-opensou que le moduleopensle paquet. - C'est par objet. Appeler
setAccessible(true)n'affecte que l'instance deFieldsur laquelle vous l'avez appelé, pas le champ globalement. UnFieldfraîchement obtenu pour le même membre commence à nouveau verrouillé.
Écrire des champs final
Les champs final constituent un cas particulier et délicat. Pour un champ final non statique, il est parfois possible de l'écrire après setAccessible(true) :
Field f = Config.class.getDeclaredField("name"); // private final String
f.setAccessible(true);
f.set(config, "changed"); // may work…Mais il y a d'importantes mises en garde :
- Cela ne fonctionne pas pour les constantes
static finalprimitives ouString— celles-ci sont intégrées par le compilateur à chaque point d'utilisation, donc même si vous modifiez le champ, les lectures déjà compilées ne le refléteront pas. - La JVM et le JIT supposent que les champs
finalne changent jamais ; les muter est un comportement indéfini pour la visibilité et peut être optimisé sans effet. - Les JDK modernes l'interdisent de plus en plus catégoriquement.
La règle honnête : ne mutez pas les champs final de manière reflective en production. Les frameworks de sérialisation qui le font (pour reconstruire des objets immuables) utilisent la mécanique de bas niveau Unsafe/VarHandle et en acceptent délibérément le risque. L'exemple ci-dessous montre le cas d'un final d'instance fonctionnant pour illustrer le mécanisme, pas comme une recommandation.
Exemple complet : un mini-mappeur basé sur les champs
Le programme reflète sur les champs déclarés d'un objet pour construire une Map<String,Object> (un mini-sérialiseur), puis prend une map et réécrit ses valeurs dans une instance fraîche (un mini-désérialiseur) — en accédant aux champs private tout au long, sans aucun getter ou setter.
Ce qu'il faut retenir de l'exécution :
toMapa produit un instantané de chaque champ d'instance sans un seul getter —getDeclaredFields()combiné àsetAccessible(true)a atteint directement l'étatprivate. C'est mécaniquement ce que font Jackson et Gson lorsqu'ils sont configurés pour l'accès par champs. La classe n'a besoin d'aucune API spéciale ; la reflection fournit l'API générique.- Le champ statique
counta été exclu parce que la boucle testaitModifier.isStatic. Les sérialiseurs ignorent systématiquement les champsstatic,transientet synthétiques ; le jeu de bits des modificateurs permet de prendre ces décisions de façon uniforme plutôt que de coder en dur les noms de champs. fromMapa écrit le champprivate final currencyaprèssetAccessible(true)et l'effet a été pris en compte — illustrant le mécanisme des champsfinald'instance. Cela a fonctionné uniquement parce quecurrencyest un final non statique réassigné avant que tout optimiseur n'en suppose la constance ; se fier à cela dans du vrai code est fragile, et les constantesstatic finaln'auraient pas bougé.- La lecture des métadonnées (
bal.getType(),Modifier.toString(...),isFinal(...)) n'a nécessité aucune instance d'Account— unFielddécrit la déclaration, qui est la même pour chaque objet de la classe. Les valeurs nécessitent une instance ; la forme, non. - Le
getInt(rebuilt)typé a retourné la primitive directement sans boxing, et la lecture du champstatica utilisécnt.get(null)— passernullcomme cible est la convention pour les statiques. Choisir l'accesseur typé pour les primitives évite une allocation par lecture, ce qui compte dans les chemins de sérialisation critiques.