CodeGym/Blog Java/Random-FR/Exemples de réflexion
Auteur
Alex Vypirailenko
Java Developer at Toshiba Global Commerce Solutions

Exemples de réflexion

Publié dans le groupe Random-FR
membres
Peut-être avez-vous rencontré le concept de "réflexion" dans la vie ordinaire. Ce mot fait généralement référence au processus d'auto-étude. En programmation, cela a une signification similaire - c'est un mécanisme pour analyser les données d'un programme, et même changer la structure et le comportement d'un programme, pendant que le programme est en cours d'exécution. Exemples de réflexion - 1 L'important ici est que nous le faisons au moment de l'exécution, pas au moment de la compilation. Mais pourquoi examiner le code à l'exécution ? Après tout, vous pouvez déjà lire le code :/ Il y a une raison pour laquelle l'idée de réflexion n'est peut-être pas immédiatement claire : jusqu'à présent, vous saviez toujours avec quelles classes vous travailliez. Par exemple, vous pouvez écrire une Catclasse :
package learn.codegym;

public class Cat {

   private String name;
   private int age;

   public Cat(String name, int age) {
       this.name = name;
       this.age = age;
   }

   public void sayMeow() {

       System.out.println("Meow!");
   }

   public void jump() {

       System.out.println("Jump!");
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public int getAge() {
       return age;
   }

   public void setAge(int age) {
       this.age = age;
   }

@Override
public String toString() {
   return "Cat{" +
           "name='" + name + '\'' +
           ", age=" + age +
           '}';
}

}
Vous savez tout à ce sujet et vous pouvez voir les champs et les méthodes dont il dispose. Supposons que vous deviez soudainement introduire d'autres classes d'animaux dans le programme. Vous pouvez probablement créer une structure d'héritage de classe avec une Animalclasse parent pour plus de commodité. Auparavant, nous avions même créé une classe représentant une clinique vétérinaire, à laquelle nous pouvions passer un Animalobjet (instance d'une classe parent), et le programme traitait l'animal de manière appropriée selon qu'il s'agissait d'un chien ou d'un chat. Même s'il ne s'agit pas des tâches les plus simples, le programme est capable d'apprendre toutes les informations nécessaires sur les classes au moment de la compilation. Ainsi, lorsque vous passez un Catobjet aux méthodes de la classe clinique vétérinaire dans lamain()méthode, le programme sait déjà qu'il s'agit d'un chat, pas d'un chien. Imaginons maintenant que nous sommes confrontés à une tâche différente. Notre objectif est d'écrire un analyseur de code. Nous devons créer une CodeAnalyzerclasse avec une seule méthode : void analyzeObject(Object o). Cette méthode doit :
  • déterminer la classe de l'objet qui lui est passé et afficher le nom de la classe sur la console ;
  • déterminer les noms de tous les champs de la classe passée, y compris ceux privés, et les afficher sur la console ;
  • déterminer les noms de toutes les méthodes de la classe passée, y compris les méthodes privées, et les afficher sur la console.
Cela ressemblera à ceci :
public class CodeAnalyzer {

   public static void analyzeClass(Object o) {

       // Print the name of the class of object o
       // Print the names of all variables of this class
       // Print the names of all methods of this class
   }

}
Nous pouvons maintenant voir clairement en quoi cette tâche diffère des autres tâches que vous avez résolues précédemment. Avec notre objectif actuel, la difficulté réside dans le fait que ni nous ni le programme ne savons exactement ce qui sera transmis auanalyzeClass()méthode. Si vous écrivez un tel programme, d'autres programmeurs commenceront à l'utiliser et ils pourraient transmettre n'importe quoi à cette méthode - n'importe quelle classe Java standard ou toute autre classe qu'ils écrivent. La classe passée peut avoir n'importe quel nombre de variables et de méthodes. En d'autres termes, nous (et notre programme) n'avons aucune idée des classes avec lesquelles nous allons travailler. Mais encore, nous devons terminer cette tâche. Et c'est là que l'API Java Reflection standard vient à notre aide. L'API Reflection est un outil puissant du langage. La documentation officielle d'Oracle recommande que ce mécanisme ne soit utilisé que par des programmeurs expérimentés qui savent ce qu'ils font. Vous comprendrez bientôt pourquoi nous donnons ce genre d'avertissement à l'avance :) Voici une liste de choses que vous pouvez faire avec l'API Reflection :
  1. Identifier/déterminer la classe d'un objet.
  2. Obtenez des informations sur les modificateurs de classe, les champs, les méthodes, les constantes, les constructeurs et les superclasses.
  3. Découvrez quelles méthodes appartiennent à une ou plusieurs interfaces implémentées.
  4. Créez une instance d'une classe dont le nom de classe n'est pas connu tant que le programme n'est pas exécuté.
  5. Obtenez et définissez la valeur d'un champ d'instance par son nom.
  6. Appelez une méthode d'instance par son nom.
Liste impressionnante, hein ? :) Note:le mécanisme de réflexion peut faire tout cela "à la volée", quel que soit le type d'objet que nous transmettons à notre analyseur de code ! Explorons les capacités de l'API Reflection en examinant quelques exemples.

Comment identifier/déterminer la classe d'un objet

Commençons par les bases. Le point d'entrée du moteur de réflexion Java est la Classclasse. Oui, ça a l'air vraiment drôle, mais c'est ce qu'est la réflexion :) En utilisant la Classclasse, nous déterminons d'abord la classe de tout objet passé à notre méthode. Essayons de faire ceci :
import learn.codegym.Cat;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println(clazz);
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Fluffy", 6));
   }
}
Sortie console :
class learn.codegym.Cat
Faites attention à deux choses. Tout d'abord, nous avons délibérément placé la Catclasse dans un package séparé learn.codegym. Vous pouvez maintenant voir que la getClass()méthode renvoie le nom complet de la classe. Deuxièmement, nous avons nommé notre variable clazz. Cela semble un peu étrange. Il serait logique de l'appeler "classe", mais "classe" est un mot réservé en Java. Le compilateur ne permettra pas aux variables d'être appelées ainsi. Nous avons dû contourner cela d'une manière ou d'une autre :) Pas mal pour commencer ! Qu'avions-nous d'autre sur cette liste de capacités ?

Comment obtenir des informations sur les modificateurs de classe, les champs, les méthodes, les constantes, les constructeurs et les superclasses.

Maintenant, les choses deviennent plus intéressantes ! Dans la classe actuelle, nous n'avons pas de constantes ni de classe parent. Ajoutons-les pour créer une image complète. Créez la Animalclasse parent la plus simple :
package learn.codegym;
public class Animal {

   private String name;
   private int age;
}
Et nous allons faire Cathériter notre classe Animalet ajouter une constante :
package learn.codegym;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Feline family";

   private String name;
   private int age;

   // ...the rest of the class
}
Maintenant, nous avons l'image complète! Voyons de quoi la réflexion est capable :)
import learn.codegym.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Class name: " + clazz);
       System.out.println("Class fields: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Parent class: " + clazz.getSuperclass());
       System.out.println("Class methods: " + Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Class constructors: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Fluffy", 6));
   }
}
Voici ce que nous voyons sur la console :
Class name:  class learn.codegym.Cat
Class fields: [private static final java.lang.String learn.codegym.Cat.ANIMAL_FAMILY, private java.lang.String learn.codegym.Cat.name, private int learn.codegym.Cat.age]
Parent class: class learn.codegym.Animal
Class methods: [public java.lang.String learn.codegym.Cat.getName(), public void learn.codegym.Cat.setName(java.lang.String), public void learn.codegym.Cat.sayMeow(), public void learn.codegym.Cat.setAge(int), public void learn.codegym.Cat.jump(), public int learn.codegym.Cat.getAge()]
Class constructors: [public learn.codegym.Cat(java.lang.String, int)]
Regardez toutes ces informations détaillées sur les classes que nous avons pu obtenir ! Et pas seulement des informations publiques, mais aussi des informations privées ! Note: privateles variables sont également affichées dans la liste. Notre « analyse » de la classe peut être considérée comme essentiellement complète : nous utilisons la analyzeObject()méthode pour apprendre tout ce que nous pouvons. Mais ce n'est pas tout ce que nous pouvons faire avec la réflexion. Nous ne nous limitons pas à une simple observation, nous passons à l'action ! :)

Comment créer une instance d'une classe dont le nom de classe n'est pas connu tant que le programme n'est pas exécuté.

Commençons par le constructeur par défaut. Notre Catclasse n'en a pas encore, alors ajoutons-la :
public Cat() {

}
Voici le code pour créer un Catobjet en utilisant la réflexion ( createCat()méthode):
import learn.codegym.Cat;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

   public static Cat createCat() throws IOException, IllegalAccessException, InstantiationException, ClassNotFoundException {

       BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
       String className = reader.readLine();

       Class clazz = Class.forName(className);
       Cat cat = (Cat) clazz.newInstance();

       return cat;
   }

public static Object createObject() throws Exception {

   BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
   String className = reader.readLine();

   Class clazz = Class.forName(className);
   Object result = clazz.newInstance();

   return result;
}

   public static void main(String[] args) throws IOException, IllegalAccessException, ClassNotFoundException, InstantiationException {
       System.out.println(createCat());
   }
}
Entrée console :
learn.codegym.Cat
Sortie console :
Cat{name='null', age=0}
Ce n'est pas une erreur : les valeurs de nameet agesont affichées sur la console car nous avons écrit du code pour les afficher dans la toString()méthode de la Catclasse. On lit ici le nom d'une classe dont on va créer l'objet depuis la console. Le programme reconnaît le nom de la classe dont l'objet doit être créé. Exemples de réflexion - 3Par souci de concision, nous avons omis le code de gestion des exceptions approprié, qui prendrait plus de place que l'exemple lui-même. Dans un vrai programme, bien sûr, vous devez gérer les situations impliquant des noms mal saisis, etc. Le constructeur par défaut est assez simple, donc comme vous pouvez le voir, il est facile de l'utiliser pour créer une instance de la classe :) Utilisation de la newInstance()méthode , nous créons un nouvel objet de cette classe. C'est une autre affaire si leCatLe constructeur prend des arguments en entrée. Supprimons le constructeur par défaut de la classe et essayons à nouveau d'exécuter notre code.
null
java.lang.InstantiationException: learn.codegym.Cat
at java.lang.Class.newInstance(Class.java:427)
Quelque chose s'est mal passé ! Nous avons eu une erreur car nous avons appelé une méthode pour créer un objet en utilisant le constructeur par défaut. Mais nous n'avons pas un tel constructeur maintenant. Ainsi, lorsque la newInstance()méthode s'exécute, le mécanisme de réflexion utilise notre ancien constructeur avec deux paramètres :
public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
Mais nous n'avons rien fait avec les paramètres, comme si nous les avions complètement oubliés ! Utiliser la réflexion pour passer des arguments au constructeur nécessite un peu de "créativité":
import learn.codegym.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.codegym.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
Sortie console :
Cat{name='Fluffy', age=6}
Examinons de plus près ce qui se passe dans notre programme. Nous avons créé un tableau d' Classobjets.
Class[] catClassParams = {String.class, int.class};
Ils correspondent aux paramètres de notre constructeur (qui n'a que des paramètres Stringet int). Nous les passons à la clazz.getConstructor()méthode et accédons au constructeur souhaité. Après cela, tout ce que nous avons à faire est d'appeler la newInstance()méthode avec les arguments nécessaires, et n'oubliez pas de transtyper explicitement l'objet dans le type souhaité : Cat.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
Maintenant, notre objet est créé avec succès ! Sortie console :
Cat{name='Fluffy', age=6}
Avance tout de suite :)

Comment obtenir et définir la valeur d'un champ d'instance par nom.

Imaginez que vous utilisez une classe écrite par un autre programmeur. De plus, vous n'avez pas la possibilité de le modifier. Par exemple, une bibliothèque de classes prête à l'emploi empaquetée dans un JAR. Vous pouvez lire le code des classes, mais vous ne pouvez pas le modifier. Supposons que le programmeur qui a créé l'une des classes de cette bibliothèque (que ce soit notre ancienne Catclasse), n'ayant pas suffisamment dormi la nuit précédant la finalisation de la conception, a supprimé le getter et le setter pour le agechamp. Maintenant, cette classe est venue à vous. Il répond à tous vos besoins, puisque vous n'avez besoin que Catd'objets dans votre programme. Mais il faut qu'ils aient un agechamp ! C'est un problème : nous ne pouvons pas atteindre le champ, car il a leprivatemodificateur, et le getter et le setter ont été supprimés par le développeur privé de sommeil qui a créé la classe :/ Eh bien, la réflexion peut nous aider dans cette situation ! Nous avons accès au code de la Catclasse, nous pouvons donc au moins savoir quels sont ses champs et comment ils s'appellent. Armés de ces informations, nous pouvons résoudre notre problème :
import learn.codegym.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;
       try {
           clazz = Class.forName("learn.codegym.Cat");
           cat = (Cat) clazz.newInstance();

           // We got lucky with the name field, since it has a setter
           cat.setName("Fluffy");

           Field age = clazz.getDeclaredField("age");

           age.setAccessible(true);

           age.set(cat, 6);

       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchFieldException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
Comme indiqué dans les commentaires, tout avec le namechamp est simple, puisque les développeurs de classe ont fourni un setter. Vous savez déjà comment créer des objets à partir de constructeurs par défaut : nous avons le newInstance()pour cela. Mais nous devrons bricoler avec le deuxième champ. Découvrons ce qui se passe ici :)
Field age = clazz.getDeclaredField("age");
Ici, à l'aide de notre Class clazzobjet , nous accédons au agechamp via la getDeclaredField()méthode. Cela nous permet d'obtenir le champ d'âge en tant Field agequ'objet. Mais cela ne suffit pas, car nous ne pouvons pas simplement attribuer des valeurs aux privatechamps. Pour ce faire, nous devons rendre le champ accessible en utilisant la setAccessible()méthode :
age.setAccessible(true);
Une fois que nous faisons cela à un champ, nous pouvons attribuer une valeur :
age.set(cat, 6);
Comme vous pouvez le voir, notre Field ageobjet a une sorte de setter inside-out auquel nous passons une valeur int et l'objet dont le champ doit être assigné. Nous exécutons notre main()méthode et voyons :
Cat{name='Fluffy', age=6}
Excellent! Nous l'avons fait! :) Voyons ce que nous pouvons faire d'autre...

Comment appeler une méthode d'instance par son nom.

Modifions légèrement la situation dans l'exemple précédent. Disons que le Catdéveloppeur de la classe n'a pas fait d'erreur avec les getters et les setters. Tout va bien à cet égard. Maintenant, le problème est différent : il existe une méthode dont nous avons absolument besoin, mais le développeur l'a rendue privée :
private void sayMeow() {

   System.out.println("Meow!");
}
Cela signifie que si nous créons Catdes objets dans notre programme, nous ne pourrons pas appeler la sayMeow()méthode sur eux. On aura des chats qui ne miaulent pas ? C'est étrange :/ Comment pourrions-nous résoudre ce problème ? Encore une fois, l'API Reflection nous aide ! Nous connaissons le nom de la méthode dont nous avons besoin. Tout le reste est une technicité :
import learn.codegym.Cat;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {

   public static void invokeSayMeowMethod()  {

       Class clazz = null;
       Cat cat = null;
       try {

           cat = new Cat("Fluffy", 6);

           clazz = Class.forName(Cat.class.getName());

           Method sayMeow = clazz.getDeclaredMethod("sayMeow");

           sayMeow.setAccessible(true);

           sayMeow.invoke(cat);

       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }
   }

   public static void main(String[] args) {
       invokeSayMeowMethod();
   }
}
Ici, nous faisons à peu près la même chose que lorsque nous accédons à un champ privé. Tout d'abord, nous obtenons la méthode dont nous avons besoin. Il est encapsulé dans un Methodobjet :
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
La getDeclaredMethod()méthode nous permet d'accéder aux méthodes privées. Ensuite, nous rendons la méthode appelable :
sayMeow.setAccessible(true);
Et enfin, nous appelons la méthode sur l'objet souhaité :
sayMeow.invoke(cat);
Ici, notre appel de méthode ressemble à un "callback": on a l'habitude d'utiliser un point pour pointer un objet vers la méthode désirée ( cat.sayMeow()), mais quand on travaille avec la réflexion, on passe à la méthode l'objet sur lequel on veut appeler cette méthode. Qu'y a-t-il sur notre console ?
Meow!
Tout a fonctionné ! :) Vous pouvez maintenant voir les vastes possibilités que nous offre le mécanisme de réflexion de Java. Dans des situations difficiles et inattendues (comme nos exemples avec une classe d'une bibliothèque fermée), cela peut vraiment beaucoup nous aider. Mais, comme pour toute grande puissance, cela implique une grande responsabilité. Les inconvénients de la réflexion sont décrits dans une section spéciale sur le site Web d'Oracle. Il y a trois inconvénients principaux :
  1. Les performances sont moins bonnes. Les méthodes appelées à l'aide de la réflexion ont de moins bonnes performances que les méthodes appelées de manière normale.

  2. Il y a des restrictions de sécurité. Le mécanisme de réflexion nous permet de modifier le comportement d'un programme à l'exécution. Mais sur votre lieu de travail, lorsque vous travaillez sur un projet réel, vous pouvez être confronté à des limitations qui ne le permettent pas.

  3. Risque d'exposition des informations internes. Il est important de comprendre que la réflexion est une violation directe du principe d'encapsulation : elle nous permet d'accéder à des champs privés, des méthodes, etc. Je ne pense pas avoir besoin de mentionner qu'une violation directe et flagrante des principes de la POO devrait être recourue uniquement dans les cas les plus extrêmes, lorsqu'il n'existe aucun autre moyen de résoudre un problème pour des raisons indépendantes de votre volonté.

Utilisez la réflexion à bon escient et uniquement dans les situations où elle ne peut être évitée, et n'oubliez pas ses lacunes. Avec cela, notre leçon est terminée. Cela s'est avéré assez long, mais vous avez beaucoup appris aujourd'hui :)
Commentaires
  • Populaires
  • Nouveau
  • Anciennes
Tu dois être connecté(e) pour laisser un commentaire
Cette page ne comporte pas encore de commentaires