W3docs

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 only

getField/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 …User

getType() 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, autounboxes

Il 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 opens son paquet vers vous, setAccessible(true) lève une InaccessibleObjectException. Les bibliothèques vous demandent d'ajouter --add-opens ou que le module opens le paquet.
  • C'est par objet. Appeler setAccessible(true) n'affecte que l'instance de Field sur laquelle vous l'avez appelé, pas le champ globalement. Un Field fraî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 final primitives ou String — 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 final ne 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.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • toMap a produit un instantané de chaque champ d'instance sans un seul getter — getDeclaredFields() combiné à setAccessible(true) a atteint directement l'état private. 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 count a été exclu parce que la boucle testait Modifier.isStatic. Les sérialiseurs ignorent systématiquement les champs static, transient et 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.
  • fromMap a écrit le champ private final currency après setAccessible(true) et l'effet a été pris en compte — illustrant le mécanisme des champs final d'instance. Cela a fonctionné uniquement parce que currency est 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 constantes static final n'auraient pas bougé.
  • La lecture des métadonnées (bal.getType(), Modifier.toString(...), isFinal(...)) n'a nécessité aucune instance d'Account — un Field dé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 champ static a utilisé cnt.get(null) — passer null comme 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.

Exercice

Pratique
Une bibliothèque JSON désérialise dans une classe qui a des champs 'private' et pas de setters. En utilisant la reflection, elle appelle 'getDeclaredField(name)' puis 'field.set(obj, value)', mais obtient 'IllegalAccessException' sur le premier champ privé. Quel appel unique permet de corriger cela, et pourquoi est-il nécessaire ?
Une bibliothèque JSON désérialise dans une classe qui a des champs 'private' et pas de setters. En utilisant la reflection, elle appelle 'getDeclaredField(name)' puis 'field.set(obj, value)', mais obtient 'IllegalAccessException' sur le premier champ privé. Quel appel unique permet de corriger cela, et pourquoi est-il nécessaire ?
Was this page helpful?