Récupérer des champs d'une classe Java à l'aide de Reflection

1. Vue d'ensemble

La réflexion est la capacité d'un logiciel informatique à inspecter sa structure lors de l'exécution. En Java, nous y parvenons en utilisant l' API Java Reflection . Il nous permet d'inspecter les éléments d'une classe tels que les champs, les méthodes ou même les classes internes, le tout à l'exécution.

Ce didacticiel se concentrera sur la façon de récupérer les champs d'une classe Java, y compris les champs privés et hérités.

2. Récupération des champs d'une classe

Voyons d'abord comment récupérer les champs d'une classe, quelle que soit leur visibilité. Plus tard, nous verrons également comment obtenir des champs hérités.

Commençons par un exemple de classe Person avec deux champs String : lastName et firstName . Le premier est protégé (ce sera utile plus tard) tandis que le second est privé:

public class Person { protected String lastName; private String firstName; }

Nous voulons obtenir les champs lastName et firstName en utilisant la réflexion. Nous y parviendrons en utilisant la méthode Class :: getDeclaredFields . Comme son nom l'indique, cela retourne tous les champs déclarés d'une classe, sous la forme d'un tableau Field :

public class PersonAndEmployeeReflectionUnitTest { /* ... constants ... */ @Test public void givenPersonClass_whenGetDeclaredFields_thenTwoFields() { Field[] allFields = Person.class.getDeclaredFields(); assertEquals(2, allFields.length); assertTrue(Arrays.stream(allFields).anyMatch(field -> field.getName().equals(LAST_NAME_FIELD) && field.getType().equals(String.class)) ); assertTrue(Arrays.stream(allFields).anyMatch(field -> field.getName().equals(FIRST_NAME_FIELD) && field.getType().equals(String.class)) ); } }

Comme nous pouvons le voir, nous obtenons les deux champs de la classe Person . Nous vérifions leurs noms et types qui correspondent aux définitions de champs dans la classe Person .

3. Récupération des champs hérités

Voyons maintenant comment récupérer les champs hérités d'une classe Java.

Pour illustrer cela, créons une deuxième classe nommée Employee étendant Person , avec un champ qui lui est propre:

public class Employee extends Person { public int employeeId; }

3.1. Récupération de champs hérités sur une hiérarchie de classes simple

L'utilisation de Employee.class.getDeclaredFields () ne renvoie que le champ employeeId , car cette méthode ne retourne pas les champs déclarés dans les superclasses. Pour obtenir également les champs hérités, nous devons également obtenir les champs de la superclasse Person .

Bien sûr, nous pourrions utiliser la méthode getDeclaredFields () sur les classes Person et Employee et fusionner leurs résultats en un seul tableau. Mais que faire si nous ne voulons pas spécifier explicitement la superclasse?

Dans ce cas, nous pouvons utiliser une autre méthode de l' API Java Reflection : Class :: getSuperclass . Cela nous donne la superclasse d'une autre classe, sans que nous ayons besoin de savoir ce qu'est cette superclasse.

Regroupons les résultats de getDeclaredFields () sur Employee.class et Employee.class.getSuperclass () et fusionnons-les en un seul tableau:

@Test public void givenEmployeeClass_whenGetDeclaredFieldsOnBothClasses_thenThreeFields() { Field[] personFields = Employee.class.getSuperclass().getDeclaredFields(); Field[] employeeFields = Employee.class.getDeclaredFields(); Field[] allFields = new Field[employeeFields.length + personFields.length]; Arrays.setAll(allFields, i -> (i < personFields.length ? personFields[i] : employeeFields[i - personFields.length])); assertEquals(3, allFields.length); Field lastNameField = allFields[0]; assertEquals(LAST_NAME_FIELD, lastNameField.getName()); assertEquals(String.class, lastNameField.getType()); Field firstNameField = allFields[1]; assertEquals(FIRST_NAME_FIELD, firstNameField.getName()); assertEquals(String.class, firstNameField.getType()); Field employeeIdField = allFields[2]; assertEquals(EMPLOYEE_ID_FIELD, employeeIdField.getName()); assertEquals(int.class, employeeIdField.getType()); }

Nous pouvons voir ici que nous avons regroupé les deux champs Personne ainsi que le champ unique Employé .

Mais le champ privé de Personne est-il vraiment un champ hérité? Pas tellement. Ce serait la même chose pour un champ package-private . Seuls les champs publics et protégés sont considérés comme hérités.

3.2. Filtrage des champs publics et protégés

Malheureusement, aucune méthode de l'API Java ne nous permet de rassembler des champs publics et protégés d'une classe et de ses superclasses. La méthode Class :: getFields s'approche de notre objectif car elle retourne tous les champs publics d'une classe et de ses superclasses, mais pas les protégés .

La seule façon dont nous devons obtenir uniquement les champs hérités est d'utiliser la méthode getDeclaredFields () , comme nous venons de le faire, et de filtrer ses résultats en utilisant la méthode Field :: getModifiers . Celui-ci retourne un int représentant les modificateurs du champ courant. Chaque modificateur possible se voit attribuer une puissance de deux entre 2 ^ 0 et 2 ^ 7 .

Par exemple, public est 2 ^ 0 et statique est 2 ^ 3 . Par conséquent, appeler la méthode getModifiers () sur un champ public et statique renverrait 9.

Ensuite, il est possible d'effectuer une opération au niveau du bit et entre cette valeur et la valeur d'un modificateur spécifique pour voir si ce champ a ce modificateur. Si l'opération renvoie autre chose que 0, le modificateur est appliqué, sinon pas.

Nous avons de la chance car Java nous fournit une classe utilitaire pour vérifier si des modificateurs sont présents dans la valeur renvoyée par getModifiers () . Utilisons les méthodes isPublic () et isProtected () pour ne rassembler que les champs hérités dans notre exemple:

List personFields = Arrays.stream(Employee.class.getSuperclass().getDeclaredFields()) .filter(f -> Modifier.isPublic(f.getModifiers()) || Modifier.isProtected(f.getModifiers())) .collect(Collectors.toList()); assertEquals(1, personFields.size()); assertTrue(personFields.stream().anyMatch(field -> field.getName().equals(LAST_NAME_FIELD) && field.getType().equals(String.class)) );

Comme on peut le voir, le résultat ne porte plus le champ privé .

3.3. Récupération de champs hérités sur une hiérarchie de classes profonde

Dans l'exemple ci-dessus, nous avons travaillé sur une hiérarchie de classes unique. Que faisons-nous maintenant si nous avons une hiérarchie de classes plus profonde et que nous voulons rassembler tous les champs hérités?

Supposons que nous ayons une sous-classe de Employee ou une superclasse de Person - alors obtenir les champs de toute la hiérarchie nécessitera de vérifier toutes les superclasses.

Nous pouvons y parvenir en créant une méthode utilitaire qui traverse la hiérarchie, en construisant le résultat complet pour nous:

List getAllFields(Class clazz) { if (clazz == null) { return Collections.emptyList(); } List result = new ArrayList(getAllFields(clazz.getSuperclass())); List filteredFields = Arrays.stream(clazz.getDeclaredFields()) .filter(f -> Modifier.isPublic(f.getModifiers()) || Modifier.isProtected(f.getModifiers())) .collect(Collectors.toList()); result.addAll(filteredFields); return result; }

Cette méthode récursive recherchera les champs publics et protégés dans la hiérarchie des classes et retournera tout ce qui a été trouvé dans une liste .

Illustrons-le avec un petit test sur une nouvelle classe MonthEmployee , étendant celle des Employés :

public class MonthEmployee extends Employee { protected double reward; }

Cette classe définit un nouveau champ - la récompense . Compte tenu de toute la classe de hiérarchie, notre méthode devrait nous donner les définitions de champs suivantes : Person :: lastName, Employee :: employeeId et MonthEmployee :: award .

Appelons la méthode getAllFields () sur MonthEmployee :

@Test public void givenMonthEmployeeClass_whenGetAllFields_thenThreeFields() { List allFields = getAllFields(MonthEmployee.class); assertEquals(3, allFields.size()); assertTrue(allFields.stream().anyMatch(field -> field.getName().equals(LAST_NAME_FIELD) && field.getType().equals(String.class)) ); assertTrue(allFields.stream().anyMatch(field -> field.getName().equals(EMPLOYEE_ID_FIELD) && field.getType().equals(int.class)) ); assertTrue(allFields.stream().anyMatch(field -> field.getName().equals(MONTH_EMPLOYEE_REWARD_FIELD) && field.getType().equals(double.class)) ); }

Comme prévu, nous rassemblons tous les domaines publics et protégés .

4. Conclusion

Dans cet article, nous avons vu comment récupérer les champs d'une classe Java à l'aide de l' API Java Reflection .

Nous avons d'abord appris à récupérer les champs déclarés d'une classe. Après cela, nous avons vu comment récupérer ses champs de superclasse également. Ensuite, nous avons appris à filtrer les champs non publics et non protégés .

Enfin, nous avons vu comment appliquer tout cela pour rassembler les champs hérités d'une hiérarchie de classes multiples.

Comme d'habitude, le code complet de cet article est disponible sur notre GitHub.