Java Reflection : Appeler des méthodes
Inspectez et appelez des méthodes par réflexion en Java avec la classe Method.
Un objet Method décrit une méthode et — point crucial — vous permet de l'appeler : method.invoke(target, args...). C'est le cœur réflexif des lanceurs de tests (trouver les méthodes @Test, les invoquer), des frameworks qui distribuent les requêtes vers des gestionnaires par leur nom, et des ponts de scripting. Ce chapitre couvre la recherche de méthodes, la correspondance des types de paramètres qui surprend tout le monde, l'invocation de méthodes d'instance et statiques, les valeurs de retour, et la façon dont les exceptions sont encapsulées.
Si vous débutez avec la réflexion, commencez par l'introduction à la réflexion et le chapitre sur l'objet Class, car tout ici part d'un Class<?>. La lecture et l'écriture des membres de données fonctionnent de la même façon et sont couvertes dans la réflexion sur les champs.
Trouver des méthodes
Vous recherchez une méthode par nom et types de paramètres — les types de paramètres permettent à Java de distinguer les surcharges :
Class<?> c = Calc.class;
Method m1 = c.getMethod("add", int.class, int.class); // public, incl. inherited
Method m2 = c.getDeclaredMethod("secret", String.class); // any access, this class only
Method[] pub = c.getMethods(); // all public methods, incl. Object's and inherited
Method[] own = c.getDeclaredMethods(); // all access levels, declared here onlyLes objets Class des paramètres doivent correspondre exactement aux types de paramètres déclarés — il n'y a pas de résolution de surcharge ni d'élargissement. getMethod("add", Integer.class, Integer.class) ne trouvera pas add(int, int) ; vous devez passer int.class. Une mauvaise combinaison lève une NoSuchMethodException. Pour une méthode sans argument, ne passez aucun argument de classe : getMethod("toString").
Invoquer : instance, statique et arguments
invoke prend l'objet cible en premier, puis les arguments sous forme de varargs Object[] :
Calc calc = new Calc();
Method add = Calc.class.getMethod("add", int.class, int.class);
Object result = add.invoke(calc, 2, 3); // → Integer 5 (autoboxed)
int sum = (int) result; // unbox manuallyPour une méthode statique, la cible est ignorée — passez null :
Method parse = Integer.class.getMethod("parseInt", String.class);
Object n = parse.invoke(null, "42"); // → Integer 42Les arguments primitifs sont autoboxés dans le tableau Object[] ; le runtime les déboxe pour correspondre aux paramètres primitifs. La valeur de retour est toujours un Object — les primitifs reviennent boxés, les méthodes void retournent null.
Comment les exceptions remontent : InvocationTargetException
C'est le piège le plus important. Si la méthode invoquée lève une exception, invoke ne propage pas cette exception directement. Elle l'encapsule dans une InvocationTargetException, et vous récupérez la vraie exception avec getCause() :
try {
riskyMethod.invoke(target);
} catch (InvocationTargetException e) {
Throwable real = e.getCause(); // the exception the method actually threw
// handle 'real', not 'e'
}Les autres exceptions vérifiées concernent l'appel réflexif lui-même, pas le corps de la méthode :
IllegalAccessException— la méthode est inaccessible et vous n'avez pas appelésetAccessible(true).IllegalArgumentException— nombre ou types d'arguments incorrects, ou type de cible incorrect.NoSuchMethodException— levée au moment de la recherche, pas au moment de l'invocation.
Ainsi : les échecs de recherche et les erreurs d'arguments sont levés « directement », mais tout ce que le code de la méthode lance est mis en bouteille dans InvocationTargetException.
Types de retour, varargs et génériques
- Métadonnées du type de retour :
m.getReturnType()(Classeffacée) etm.getGenericReturnType()(Type, conserve les génériques). - Paramètres :
m.getParameterTypes(),m.getParameterCount(), etm.getParameters()(noms disponibles si compilé avec-parameters). - Varargs : un paramètre
String...est en réalitéString[]. Recherchez-le avecgetMethod("f", String[].class)et invoquez en passant un tableau réel, ou comptez sur le fait queinvokeacceptera un tableau final pour le slot varargs. - Méthodes bridge/synthétiques : les classes génériques génèrent des méthodes bridge cachées ; filtrez-les avec
m.isBridge()/m.isSynthetic()lors de l'énumération.
Un exemple complet : un mini-distributeur de commandes
Le programme construit un petit distributeur qui associe des commandes en chaîne à des méthodes d'un objet service, les invoque par réflexion avec des arguments analysés, gère une méthode qui lève une exception (pour montrer le déballage de InvocationTargetException), et appelle une fabrique static avec une cible null.
Ce qu'il faut retenir de l'exécution :
- La fabrique statique a été appelée avec
factory.invoke(null)— pour une méthodestatic, l'objet cible n'a pas d'importance, doncnullest la convention. Le distributeur a ensuite réutilisé le même mécanismeinvokepour les méthodes d'instance, en passant le vraicalccomme cible. Une seule API, les deux types de méthodes. divide(1, 0)n'a pas levéArithmeticExceptiondepuisinvoke. Il a levéInvocationTargetException, et la vraieArithmeticException: / by zeroa été trouvée viagetCause(). Tout framework qui appelle du code utilisateur par réflexion doit déencapsuler cela ; oublier de le faire est la raison pour laquelle vous voyez parfois uneInvocationTargetExceptiondéroutante dans une trace de pile au lieu de la vraie erreur.- La recherche de
addavec des paramètresInteger.classa échoué avecNoSuchMethodExceptionalors queadd(int,int)existe. La réflexion fait correspondre les types de paramètres exactement sans boxing ni élargissement —int.classetInteger.classsont des clés différentes. C'est le bug de réflexion le plus courant et la raison pour laquelle les littéraux primitifs.classsont importants. - La méthode
private secretn'était invocable qu'aprèsgetDeclaredMethod+setAccessible(true). Comme pour les champs, la variante de recherche (getDeclared…) et la porte d'accès (setAccessible) sont deux étapes indépendantes ; vous avez besoin des deux pour atteindre un membre privé. - Les valeurs de retour sont arrivées sous forme d'
Objectet ont été castées au site d'appel ((Calculator) factory.invoke(...)), tandis que l'intdeaddest revenu autoboxé enInteger. La réflexion n'a aucune connaissance statique des types de retour, donc l'appelant est responsable du cast/unbox — et un mauvais cast se manifeste comme uneClassCastExceptionà l'exécution, pas à la compilation.