Accès rapide :
La vidéo
Qu'est-ce que la réflexion ?
Récupérer les méta-données d'un type via la classe java.lang.Class
Lister les attributs d'une classe
Lister les attributs publics de la classe
Lister uniquement les attributs déclarés dans la classe
Lister l'ensemble des attributs disponibles sur la classe
Lister les méthodes d'une classe
Les principales possibilités proposées
Exemple de parcours de toutes les méthodes exposées par une classe
Lister les constructeurs d'une classe
Les principales possibilités proposées
Exemple de parcours de tous les constructeurs (publics ou non) exposés par une classe
Manipulation des types sur lesquels est basée notre classe
Lister la hiérarchie d'héritage
Lister les interfaces implémentées par notre classe
La réflexion dans le cadre de l'écriture de jeux de tests unitaires
Accès à un attribut privé
Accès à une méthode privée
Cette vidéo vous montre comment utiliser le moteur de réflexion Java (accessible dans le package java.lang.reflect) pour obtenir des informations descriptives sur vos types de données.
La réflexion est un concept objets permettant d'avoir accès à la structure interne de vos types. Certains langages objets ne supportent pas nativement la réflexion : c'est notamment le cas de C++. Cela est dû au fait que C++ supprime la « table des symboles » de l'exécutable produit. Cette table des symboles contient notamment la définition de tous les membres de la classe et est utile durant la phase d'édition des liens (aussi appelée « link »).
En Java, la table des symboles est requise au bon fonctionnement de votre programme, notamment à cause du fait que l'édition de liens est réalisée pendant l'exécution de votre programme (au chargement du code de la classe par le « Class Loader »). La réflexion permet donc d'accéder aux informations stockées dans cette table des symboles.
Il y a un point négatif au fait que Java propose la réflexion : il est très facile de décompiler un code Java. De nombreux décompilateurs vous sont proposés : jad, ... Si vous souhaitez vous prémunir de la décompilation, c'est compliqué en Java. Au mieux vous pouvez obfusquer (offusquer, si vous préférez) votre code Java (brouiller certains éléments de la table des symboles).
A contrario, la réflexion permet de nombreuses possibilités intéressantes du langage Java : sérialisation, garbage collector, ... Nous allons d'ailleurs en étudier certaines dans les chapitres suivants.
Le support de réflexion Java est proposé par la classe java.lang.Class
et le package java.lang.reflect
(reflect pour reflection, en
anglais).
Le point d'entrée sur le moteur de réflexion consiste à récupérer les méta-données (les données descriptives) d'un type Java : ces méta-données seront
stockées dans une instance de type java.lang.Class
.
Deux possibilités sont disponibles pour récupérer des méta-données : soit à partir d'une instance (avec la méthode getClass()
),
soit à partir d'un type Java, via un pseudo-attribut statique. Voici un exemple de code montrant ces deux possibilités.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class TestReflection { public static void main( String[] args ) throws Exception { // Récupération des méta-données à partir de la classe. Class<TestReflection> metadata = TestReflection.class; // Récupération des méta-données à partir d'une instance. TestReflection object = new TestReflection(); Class<? extends TestReflection> metadata2 = object.getClass(); // Dans les deux cas, nous avons les mêmes informations System.out.println( metadata == metadata2 ); } } |
==
utilisé entre deux instances Java compare les pointeurs. La valeur true
indique donc que c'est la même instance
qui est retournée dans les deux cas.
java.lang.Class
est générique. L'utilisation de la syntaxe <? extends TestReflection>
permet d'éviter un warning.
Nous allons maintenant chercher à travailler sur les définitions d'attributs d'une classe. Pour autant, vous avez deux possibilités : soit vous cherchez à récupérer uniquement les attributs publics d'une classe (y compris ceux hérités), soit vous cherchez à travailler que sur les attributs de la classe considérée (quel que soit le niveau de visibilité considéré).
L'exemple de code suivant permet d'afficher des informations sur l'ensemble des attributs publics d'une classe (y compris ceux hérités des classes parentes).
Ces informations sont acquises grâce à la méthode getFields()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import java.lang.reflect.Field; import java.util.Date; class Base { public int first; private int second; } public class TestReflection extends Base { private int aNumericValue; protected String aString; double aPrice; // Visibilité "package" par défaut public Date aDate; public static void main( String[] args ) throws Exception { // Récupération des méta-données à partir de la classe. Class<TestReflection> metadata = TestReflection.class; // On récupère les attributs publics disponibles à partir de la classe // (et y compris dans les classes parentes : Base et Object). Field [] attributes = metadata.getFields(); // On affiche des informations sur ces attributs publics. for( Field attribute : attributes ) { System.out.printf( "%s of type %s (isPrimitive: %b)\n", attribute.getName(), attribute.getType().getName(), attribute.getType().isPrimitive() ); } } } |
isPrimitive
permet de savoir si le type manipulé est un type élémentaire du langage ou si c'est un type objet (une instance).
Voici les résultats produits par cet exemple de code :
aDate of type java.util.Date (isPrimitive: false) first of type int (isPrimitive: true)
Si vous souhaitez retrouver les données descriptives d'un attribut public particulier, vous pouvez aussi utiliser la méthode
Class.getField( String fieldName )
. Voici un petit extrait de code.
1 2 |
Class<TestReflection> metadata = TestReflection.class; Field field = metadata.getField( "aDate" ); |
L'exemple de code suivant ne liste que, et uniquement que, les attributs déclarés dans la classe considérée. Les attributs définis dans une classe parentes,
(quelles que soient leurs visibilités) ne seront pas considérés. Ces informations sont acquises grâce à la méthode getDeclaredFields()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import java.lang.reflect.Field; import java.util.Date; class Base { public int first; private int second; } public class TestReflection extends Base { private int aNumericValue; protected String aString; double aPrice; // Visibilité package par défaut public Date aDate; public static void main( String[] args ) throws Exception { // Récupération des méta-données à partir de la classe. Class<TestReflection> metadata = TestReflection.class; // On récupère tous les attributs déclarés dans la classe Field [] attributes = metadata.getDeclaredFields(); // On affiche des informations sur les attributs. for( Field attribute : attributes ) { System.out.printf( "%s of type %s (isPrimitive: %b)\n", attribute.getName(), attribute.getType().getName(), attribute.getType().isPrimitive() ); } } } |
Voici les résultats produits par cet exemple de code :
aNumericValue of type int (isPrimitive: true) aString of type java.lang.String (isPrimitive: false) aPrice of type double (isPrimitive: true) aDate of type java.util.Date (isPrimitive: false)
Si vous souhaitez retrouver les données descriptives d'un attribut particulier, vous pouvez aussi utiliser la méthode
Class.getDeclaredField( String fieldName )
. Voici un petit extrait de code.
1 2 |
Class<TestReflection> metadata = TestReflection.class; Field field = metadata.getDeclaredField( "aDate" ); |
Ce nouvel exemple vous montre comment lister l'ensemble des attributs disponibles sur la classe (y compris ceux définis sur les classes dérivées).
La méthode Class.getSuperclass()
permet de récupérer les méta-données de la classe parente. En effectuant une remonter récursive dans les
méta-données parentes, on peut traiter l'ensemble des attributs existants.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Date; class Base { public int first; private int second; } public class TestReflection extends Base { private int aNumericValue; protected String aString; double aPrice; // Visibilité package par défaut public Date aDate; public static void main( String[] args ) throws Exception { // Récupération des méta-données à partir de la classe. Class<?> metadata = TestReflection.class; while ( true ) { // On affiche le type en cours d'analyse System.out.println( metadata.getName() ); // On récupère les attributs déclarés dans le type courant. Field [] attributes = metadata.getDeclaredFields(); // On affiche des informations sur les attributs du type. for( Field attribute : attributes ) { Class<?> attributeMetadata = attribute.getType(); System.out.printf( "\t%-10s %s of type %s (isPrimitive: %b)\n", Modifier.toString( attribute.getModifiers() ), attribute.getName(), attributeMetadata.getName(), attributeMetadata.isPrimitive() ); } // On remonte sur le type parent, s'il y en a un. metadata = metadata.getSuperclass(); if ( metadata == null ) break; } } } |
Et voici les résultats produits par cet exemple de code :
TestReflection private aNumericValue of type int (isPrimitive: true) protected aString of type java.lang.String (isPrimitive: false) aPrice of type double (isPrimitive: true) public aDate of type java.util.Date (isPrimitive: false) Base public first of type int (isPrimitive: true) private second of type int (isPrimitive: true) java.lang.Object
De la même manière que nous venons de lister les méta-données des attributs de la classe, vous pouvez parcourir l'ensemble des méthodes d'un type et
en manipuler leurs données descriptives. Ces informations seront stockées dans des instances de type java.lang.reflect.Method
.
La méthode Class.getMethods()
renvoie l'ensemble des méthodes publiques (y compris celles des classes parentes) alors de la méthode
Class.getDeclaredMethods()
renvoie l'ensemble des méthodes contenues uniquement dans cette classe (quelles ques soient leurs visibilités).
Voici les principales méthodes de la classe java.lang.Class
permettant de retrouver des données descriptives (méta-données) à propos des
méthodes d'un type.
Class.getDeclaredMethod( String methodName, Class<?> ... parameterTypes )
: renvoie des méta-données sur une méthode de la classe
considérée. Il faut, bien entendu, spécifier la signature de la méthode recherchée pour lever les ambiguïtés en cas de surcharge de méthodes.
Class.getDeclaredMethods()
: renvoie des méta-données sur l'ensemble des méthodes contenues uniquement dans cette classe (quelles que
soient leurs visibilités).
Class.getMethod( String methodName, Class<?> ... parameterTypes )
: renvoie des méta-données sur une méthode publique (y compris si
elle est définie dans une classe parente). Il faut, bien entendu, spécifier la signature de la méthode recherchée pour lever les ambiguïtés en cas de
surcharge de méthodes.
Class.getMethods()
: renvoie des méta-données sur l'ensemble des méthodes publiques (y compris celles des classes parentes).
L'exemple de code proposé ci-dessous parcourt l'ensemble de méthodes directement définies dans la classe.
On y retrouve des méthodes statiques ou non et avec une visibilité quelconque (public
, private
, ...).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import java.lang.reflect.Method; public class TestReflection { public void publicMethod() {} private void privateMethod() {} public static void publicStaticMethod() {} private static void privateStaticMethod() {} public static void main( String[] args ) throws Exception { // Récupération des méta-données à partir de la classe. Class<?> metadata = TestReflection.class; // On récupère les méthodes déclarées dans le type courant Method [] methods = metadata.getDeclaredMethods(); // On affiche des informations sur les méthodes de la classe for( Method method : methods ) { System.out.println( method.getName() ); } } } |
Et voici les résultats produits par cet exemple de code :
main publicStaticMethod publicMethod privateMethod privateStaticMethod
main
étant définie dans la classe traitée par cet exemple, il est normal de la retrouver dans les résultats produits.
Dans le cadre de l'analyse des constructeurs, il est important de se rappeler qu'ils ne s'héritent pas. Il est donc impossible, via une instance de méta-données de retrouver des informations sur les constructeurs déclarés dans les types parents.
Les données descriptives d'un constructeur seront contenues dans une instance Java de type java.lang.refect.Constructor
et il s'agit d'un type
générique.
Voici les principales méthodes de la classe java.lang.Class
permettant de retrouver des données descriptives (méta-données) à propos des
constructeurs d'un type de données Java.
Class.getDeclaredConstructor( String constructorName, Class<?> ... parameterTypes )
: renvoie des méta-données sur un constructeur
de la classe considérée. Il faut, bien entendu, spécifier la signature du constructeur recherché pour lever les ambiguïtés en cas de surcharge.
Class.getDeclaredConstructors()
: renvoie des méta-données sur l'ensemble des constructeurs contenus dans cette classe
(quelles que soient leurs visibilités).
Class.getConstructor( String constructorName, Class<?> ... parameterTypes )
: renvoie des méta-données sur un constructeur public
Il faut, bien entendu, spécifier la signature du constructeur recherché pour lever les ambiguïtés en cas de surcharge.
Class.getConstructors()
: renvoie des méta-données sur l'ensemble des constructeurs publics de la classe.
Le programme ci-dessous liste l'ensemble des constructeurs présents dans la classe considérée.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import java.lang.reflect.Constructor; public class TestReflection { public static void main( String[] args ) throws Exception { // Récupération des méta-données à partir de la classe. Class<?> metadata = TestReflection.class; // On récupère les constructeurs déclarés sur le type courant Constructor<?> [] constructors = metadata.getDeclaredConstructors(); // On affiche des informations sur ces constructeurs for( Constructor<?> constructor : constructors ) { System.out.println( constructor.toString() ); } } } |
Et voici le résultat produit par ce programme :
public TestReflection()
Une classe est définie à partir d'un ou plusieurs types parents. Bien entendu, il n'y aura qu'un seul type parent en termes d'héritage, mais n'oubliez pas qu'une classe peut implémenter une ou plusieurs interfaces. Pour rappel, voici un exemple de déclaration de classe en Java.
1 2 3 4 5 |
public class Demo extends Parent implements Interface1, Interface2 { // TODO: implémentation de la classe à terminer. } |
L'API de réflexion Java permet de retrouver la classe parente que l'on étend, mais aussi l'ensemble des interfaces que la classe implémente.
L'exemple ci-dessus cherche à afficher toute la hiérarchie d'héritage pour une classe donnée. Il est donc aussi nécessaire de retrouver les données descriptives
des types parents : c'est ce qui est fait dans la boucle while
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import java.io.Serializable; class Base { } public class TestReflection extends Base { public static void main( String[] args ) throws Exception { // Récupération des méta-données à partir de la classe. Class<?> metadata = TestReflection.class; StringBuilder builder = new StringBuilder( metadata.getName() ); while ( true ) { // On remonte sur le type parent, s'il y en a un. metadata = metadata.getSuperclass(); if ( metadata == null ) break; // On insert en tête du StringBuilder le nouveau niveau d'héritage builder.insert( 0, metadata.getName() + " -> " ); } // On affiche la hiérarchie d'héritage System.out.println( builder.toString() ); } } |
Et voici les résultats produits par cet exemple :
java.lang.Object -> Base -> TestReflection
Nous allons maintenant chercher à retrouver la liste des interfaces implémentées par une classe donnée. Nous ne considérerons que les interfaces directement implémentées par la classe considérée, mais en mixant le code ci-dessous avec celui de l'exemple précédent, vous devriez être en mesure de lister l'intégralité des interfaces implémentées sur tous les niveaux d'héritage.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import java.io.Serializable; interface NotListed {} class Base implements NotListed { } public class TestReflection extends Base implements Runnable, Serializable { @Override public void run() {} public static void main( String[] args ) throws Exception { // Récupération des méta-données à partir de la classe. Class<?> metadata = TestReflection.class; // On retrouve toutes les interfaces directement implémentées par le type considéré. Class<?> [] implementedInterfaces = metadata.getInterfaces(); // On affiche ces interfaces. for( Class<?> interfaceMetadata : implementedInterfaces ) { System.out.println( interfaceMetadata.getName() ); } } } |
Et voici les résultats produits par cet exemple.
java.lang.Runnable java.io.Serializable
Pour clore ce chapitre, je voudrais vous montrer une possibilité qui peut être très utiles dans certains cas : la possibilité d'accéder à des éléments privés de la classe à l'extérieur de celle-ci. Bien entendu, si une telle possibilité existe, elle serait en totale contradiction avec le principe d'encapsulation. Mais elle existe bel et bien. Du coup, notez bien que les exemples de code suivants sont à utiliser avec très grande sagesse.
Du coup, la bonne question à se poser est quand pouvons-nous, de manière acceptable, accéder à un membre privé d'une classe et donc violer le principe d'encapsulation. Un cas concret, c'est la mise en oeuvre d'une batterie de tests unitaires. Nous reviendrons plus sérieusement sur ce sujet dans des futurs chapitres, avec notamment la présentation de JUnit. Mais nous allons quand même imaginer maintenant que nous sommes en train de coder de telles procédures de tests.
Dans la théorie des tests, il existe, entre autre, deux catégories de tests : les tests « boîtes noires » et les tests « boîtes blanches ». Dans un test « boîte noire », le SUT (System Under Test ou System Under Test) est considéré comme étant opaque. On y envoie des inputs (des données d'entrées) et on vérifie simplement que les outputs (les résultats) sont correctes. Au contraire, dans un test « boîte blanches » on vérifie durant l'exécution d'un test, l'état interne du SUT est bien cohérent : l'état interne d'un composant logiciel étant souvent privé, il est nécessaire de passer outre.
L'exemple de code suivant simule un scénario de test boîte blanche et l'on tente de vérifier l'état interne du SUT. Le problème c'est que ça ne va pas marcher.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
import java.lang.reflect.Field; class Sut { private boolean stateToVerify = false; public boolean methodToTest( boolean input ) { stateToVerify = true; return ! input; } } public class TestReflection { public static void main( String[] args ) throws Exception { // On monte le scénario de tests Sut sut = new Sut(); boolean result = sut.methodToTest( true ); // On vérifie le résultat produit assert result == false; // On vérifie l'état interne du composant en test Class<Sut> metadata = Sut.class; Field probe = metadata.getDeclaredField( "stateToVerify" ); boolean value = probe.getBoolean( sut ); // <- ne marche pas assert value == true; } } |
Si vous lancez le programme ci-dessus, vous noterez les résultats suivants
$> java -ea TestReflection Exception in thread "main" java.lang.IllegalAccessException: Class TestReflection can not access a member of class Sut with modifiers "private" at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102) at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296) at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288) at java.lang.reflect.Field.getBoolean(Field.java:425) at TestReflection.main(TestReflection.java:30)
-ea
(enable assertions) de la JVM permet d'activer la vérification des assertions.
N'oubliez donc pas cette option si vous voulez que l'assert
fasse son travail.
Si on analyse le message d'erreur produit, on constate que l'accès à un élément privé est quand même vérifié et du coup une erreur est produite.
En fait, dans les méta-données, on retrouve aussi le niveau de visibilité. Par contre, dans la donnée, il n'y a pas d'information de cette nature.
Si on indique au moteur de réflexion de ne pas tenir compte de la visibilité, on autorisera alors l'accès à l'attribut. Cela se fait en rajoutant un appel
à la méthode probe.setAccessible( true )
. Voici l'exemple de test désécurisé.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
import java.lang.reflect.Field; interface NotListed {} class Sut { private boolean stateToVerify = false; public boolean methodToTest( boolean input ) { stateToVerify = true; return ! input; } } public class TestReflection { public static void main( String[] args ) throws Exception { // On monte le scénario de tests Sut sut = new Sut(); boolean result = sut.methodToTest( true ); // On vérifie le résultat produit assert result == false; // On vérifie l'état interne du composant en test Class<Sut> metadata = Sut.class; Field probe = metadata.getDeclaredField( "stateToVerify" ); probe.setAccessible( true ); boolean value = probe.getBoolean( sut ); assert value == true; } } |
Si vous lancez le test, cette fois-ci tout doit bien se passer et aucun affichage ne sera produit (c'est notre manière de dire que tout va bien).
Un autre besoin, quand on écrit des scénarios de tests, est de vérifier qu'une méthode privée fonctionne correctement.
Il serait dommage de changer la visibilité de la méthode juste pour l'écriture du test : dans ce cas, conservez la visibilité private
et
faite en sorte que votre test contourne cette visibilité restreinte. Cela passe aussi par l'emploi d'une méthode setAccessible
sur l'objet
méta-données associé à la méthode. En voici un exemple.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import java.lang.reflect.Method; interface NotListed {} class Sut { private void doSomething( int a, int b ) { System.out.println( "On passe bien dans la méthode privée" ); System.out.println( "avec a == " + a + " et b == " + b ); } } public class TestReflection { public static void main( String[] args ) throws Exception { Sut sut = new Sut(); Class<Sut> metadata = Sut.class; Method doSomethingMethod = metadata.getDeclaredMethod( "doSomething", int.class, int.class ); doSomethingMethod.setAccessible( true ); doSomethingMethod.invoke( sut, 10, 20 ); } } |
Et voici les résultats produits par cet exemple.
On passe bien dans la méthode privée avec a == 10 et b == 20
Pour conclure, gardez en mémoire qu'il est hors de question d'utiliser la réflexion pour utiliser des membres privés dans un code applicatif traditionnel. Sinon, faite directement du C, ce sera mieux pour vous ;-).
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :