JavaRush /Blog Java /Random-FR /Réflexion en Java - Exemples d'utilisation

Réflexion en Java - Exemples d'utilisation

Publié dans le groupe Random-FR
Vous avez peut-être rencontré le concept de « réflexion » dans la vie de tous les jours. Habituellement, ce mot fait référence au processus d’étude de soi. En programmation, cela a une signification similaire : il s'agit d'un mécanisme permettant d'examiner les données d'un programme, ainsi que de modifier la structure et le comportement du programme lors de son exécution. L'important ici est que cela se fasse au moment de l'exécution, pas au moment de la compilation. Mais pourquoi examiner le code au moment de l’exécution ? Vous le voyez déjà :/ Exemples d'utilisation de Réflexion - 1L'idée de réflexion n'est peut-être pas immédiatement claire pour une raison : jusqu'à ce moment, vous connaissiez toujours les classes avec lesquelles vous travailliez. Eh bien, par exemple, vous pourriez écrire une classeCat :
package learn.javarush;

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, vous voyez de quels domaines et méthodes il dispose. Vous pouvez sûrement créer un système d'héritage avec une classe commune pour plus de commodité Animal, si soudainement le programme a besoin d'autres classes d'animaux. Auparavant, nous avions même créé une classe de clinique vétérinaire dans laquelle vous pouviez transmettre un objet parent Animal, et le programme traiterait l'animal selon qu'il s'agissait d'un chien ou d'un chat. Bien que ces tâches ne soient pas très simples, le programme apprend toutes les informations dont il a besoin sur les classes au moment de la compilation. Ainsi, lorsque vous main()passez un objet dans une méthode Cataux méthodes de la classe clinique vétérinaire, le programme sait déjà qu'il s'agit d'un chat et non d'un chien. Imaginons maintenant que nous soyons confrontés à une autre tâche. Notre objectif est d'écrire un analyseur de code. Nous devons créer une classe CodeAnalyzeravec une seule méthode - void analyzeClass(Object o). Cette méthode devrait :
  • déterminer à quelle classe l'objet lui a été transmis et afficher le nom de la classe dans la console ;
  • déterminer les noms de tous les champs de cette classe, y compris les champs privés, et les afficher dans la console ;
  • déterminez les noms de toutes les méthodes de cette classe, y compris les méthodes privées, et affichez-les dans la console.
Cela ressemblera à ceci :
public class CodeAnalyzer {

   public static void analyzeClass(Object o) {

       //Вывести название класса, к которому принадлежит an object o
       //Вывести названия всех переменных этого класса
       //Вывести названия всех методов этого класса
   }

}
La différence entre ce problème et le reste des problèmes que vous avez résolus auparavant est désormais visible. Dans ce cas, la difficulté réside dans le fait que ni vous ni le programme ne savez exactement ce qui sera transmis à la méthode analyzeClass(). Vous écrivez un programme, d'autres programmeurs commenceront à l'utiliser, qui pourront tout transmettre à cette méthode - n'importe quelle classe Java standard ou n'importe quelle classe qu'ils ont écrite. Cette classe peut avoir n'importe quel nombre de variables et de méthodes. En d’autres termes, dans ce cas, nous (et notre programme) n’avons aucune idée des classes avec lesquelles nous allons travailler. Et pourtant, nous devons résoudre ce problème. Et ici, la bibliothèque Java standard nous vient en aide - l'API Java Reflection. L'API Reflection est une fonctionnalité de langage puissante. La documentation officielle d'Oracle indique qu'il est recommandé d'utiliser ce mécanisme uniquement par des programmeurs expérimentés qui comprennent très bien ce qu'ils font. Vous comprendrez bientôt pourquoi nous recevons soudainement de tels avertissements à l'avance :) Voici une liste de ce qui peut être fait en utilisant l'API Reflection :
  1. Connaître/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 à la ou aux interfaces implémentées.
  4. Créez une instance d'une classe lorsque le nom de la classe est inconnu jusqu'à l'exécution du programme.
  5. Obtenez et définissez la valeur d'un champ d'objet par son nom.
  6. Appelez la méthode d'un objet par son nom.
Une liste impressionnante, hein ? :) Faites attention:Le mécanisme de réflexion est capable de faire tout cela « à la volée » quel que soit l'objet de classe que nous transmettons à notre analyseur de code ! Examinons les capacités de l'API Reflection avec des exemples.

Comment connaître/déterminer la classe d'un objet

Commençons par les bases. Le point d'entrée du mécanisme de réflexion de Java est le Class. Oui, ça a l'air vraiment drôle, mais c'est à ça que sert la réflexion :) En utilisant une classe Class, nous déterminons tout d'abord la classe de tout objet passé à notre méthode. Essayons ça:
import learn.javarush.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("Barsik", 6));
   }
}
Sortie de la console :

class learn.javarush.Cat
Faites attention à deux choses. Tout d'abord, nous avons délibérément placé la classe Catdans un package séparé. learn.javarush;Vous pouvez maintenant voir qu'il getClass()renvoie le nom complet de la classe. Deuxièmement, nous avons nommé notre variable clazz. Ça a l'air un peu étrange. Bien sûr, il devrait être appelé « classe », mais « classe » est un mot réservé dans le langage Java, et le compilateur ne permettra pas que les variables soient appelées de cette façon. Il fallait que je m'en sorte :) Bon, ce n'est pas un mauvais début ! Qu’avions-nous d’autre sur la liste des possibilités ?

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

C'est déjà plus intéressant ! Dans la classe actuelle, nous n’avons ni constantes ni classe parent. Ajoutons-les pour être complet. Créons la classe parent la plus simpleAnimal :
package learn.javarush;
public class Animal {

   private String name;
   private int age;
}
Et ajoutons Catl'héritage Animalet une constante à notre classe :
package learn.javarush;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Семейство кошачьих";

   private String name;
   private int age;

   //...остальная часть класса
}
Nous avons maintenant un ensemble complet ! Essayons les possibilités de réflexion :)
import learn.javarush.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Name класса: " + clazz);
       System.out.println("Поля класса: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Родительский класс: " + clazz.getSuperclass());
       System.out.println("Методы класса: " +  Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Конструкторы класса: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Barsik", 6));
   }
}
Voici ce que nous obtenons dans la console :
Name класса: class learn.javarush.Cat
Поля класса: [private static final java.lang.String learn.javarush.Cat.ANIMAL_FAMILY, private java.lang.String learn.javarush.Cat.name, private int learn.javarush.Cat.age]
Родительский класс: class learn.javarush.Animal
Методы класса: [public java.lang.String learn.javarush.Cat.getName(), public void learn.javarush.Cat.setName(java.lang.String), public void learn.javarush.Cat.sayMeow(), public void learn.javarush.Cat.setAge(int), public void learn.javarush.Cat.jump(), public int learn.javarush.Cat.getAge()]
Конструкторы класса: [public learn.javarush.Cat(java.lang.String,int)]
Nous avons reçu tellement d’informations détaillées sur la classe ! Et pas seulement sur les parties publiques, mais aussi sur les parties privées. Faites attention: private-les variables sont également affichées dans la liste. En fait, « l’analyse » de la classe peut être considérée comme terminée à ce stade : maintenant, en utilisant la méthode, analyzeClass()nous apprendrons tout ce qui est possible. Mais ce ne sont pas toutes les possibilités qui s’offrent à nous lorsque nous travaillons avec réflexion. Ne nous limitons pas à la simple observation et passons à l’action active ! :)

Comment créer une instance d'une classe si le nom de la classe est inconnu avant l'exécution du programme

Commençons par le constructeur par défaut. Ce n'est pas encore dans notre classe Cat, alors ajoutons-le :
public Cat() {

}
Voici à quoi ressemblerait le code pour créer un objet Caten utilisant la réflexion (méthode createCat()) :
import learn.javarush.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());
   }
}
Entrez dans la console :

learn.javarush.Cat
Sortie de la console :

Cat{name='null', age=0}
Ce n'est pas une erreur : les valeurs nameet agesont affichées dans la console car nous avons programmé leur sortie dans la méthode toString()de classe Cat. Ici, nous lisons le nom de la classe dont nous allons créer l'objet depuis la console. Le programme en cours d'exécution apprend le nom de la classe dont il va créer l'objet. Exemples d'utilisation de Réflexion - 3Par souci de concision, nous avons omis le code permettant de gérer correctement les exceptions afin qu'il ne prenne pas plus de place que l'exemple lui-même. Dans un programme réel, bien sûr, cela vaut vraiment la peine de gérer les situations où des noms incorrects sont saisis, etc. Le constructeur par défaut est une chose assez simple, donc créer une instance d'une classe qui l'utilise, comme vous pouvez le voir, n'est pas difficile :) Et en utilisant la méthode, newInstance()nous créons un nouvel objet de cette classe. C'est une autre affaire si le constructeur de classe Catprend des paramètres 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.javarush.Cat
  at java.lang.Class.newInstance(Class.java:427)
Quelque chose s'est mal passé ! Nous avons reçu une erreur car nous avons appelé une méthode pour créer un objet via le constructeur par défaut. Mais maintenant, nous n’avons plus un tel designer. Cela signifie que lorsque la méthode fonctionne, newInstance()le mécanisme de réflexion utilisera 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 ! Pour les transmettre au constructeur par réflexion, vous devrez le modifier un peu :
import learn.javarush.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.javarush.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 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 de la console :

Cat{name='Barsik', age=6}
Regardons de plus près ce qui se passe dans notre programme. Nous avons créé un tableau d'objets Class.
Class[] catClassParams = {String.class, int.class};
Ils correspondent aux paramètres de notre constructeur (nous avons juste les paramètres Stringet int). Nous les transmettons à la méthode clazz.getConstructor()et avons accès au constructeur requis. Après cela, il ne reste plus qu'à appeler la méthode newInstance()avec les paramètres nécessaires et n'oubliez pas de convertir explicitement l'objet dans la classe dont nous avons besoin - Cat.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
En conséquence, notre objet sera créé avec succès ! Sortie de la console :

Cat{name='Barsik', age=6}
Allons-nous en :)

Comment obtenir et définir la valeur d'un champ d'objet par son nom

Imaginez que vous utilisez une classe écrite par un autre programmeur. Cependant, vous n'avez pas la possibilité de le modifier. Par exemple, une bibliothèque de classes prête à l'emploi conditionnée dans un JAR. Vous pouvez lire le code de la classe, mais vous ne pouvez pas le modifier. Le programmeur qui a créé la classe dans cette bibliothèque (que ce soit notre ancienne classe Cat) n'a pas suffisamment dormi avant la conception finale et a supprimé les getters et setters pour le champ age. Maintenant, ce cours est venu à vous. Il répond pleinement à vos besoins, car vous n'avez besoin que d'objets dans le programme Cat. Mais vous en avez besoin avec ce même champ age! C'est un problème : nous ne pouvons pas atteindre le champ, car il a un modificateur private, et les getters et setters ont été supprimés par le développeur potentiel de cette classe :/ Eh bien, la réflexion peut aussi nous aider dans cette situation ! CatNous avons accès au code de la classe : nous pouvons au moins savoir de quels champs elle dispose et comment ils s'appellent. Armés de ces informations, nous résolvons notre problème :
import learn.javarush.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

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

           //с полем name нам повезло - для него в классе есть setter
           cat.setName("Barsik");

           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 le commentaire, nametout est simple avec le champ : les développeurs de classes ont fourni un setter pour cela. Vous savez aussi déjà comment créer des objets à partir de constructeurs par défaut : il existe une méthode pour cela newInstance(). Mais il faudra bricoler le deuxième champ. Voyons ce qui se passe ici :)
Field age = clazz.getDeclaredField("age");
Ici, en utilisant notre objet Class clazz, nous accédons au champ ageen utilisant le getDeclaredField(). Cela nous donne la possibilité d'obtenir le champ age en tant qu'objet Field age. Mais cela ne suffit pas encore, car privateil n’est pas possible d’attribuer simplement des valeurs aux champs. Pour cela, vous devez rendre le champ « disponible » grâce à la méthode setAccessible():
age.setAccessible(true);
Les champs pour lesquels cela est fait peuvent se voir attribuer des valeurs :
age.set(cat, 6);
Comme vous pouvez le constater, nous avons une sorte de setter renversé : on attribue au champ Field agesa valeur, et on lui passe également l'objet auquel ce champ doit être attribué. Exécutons notre méthode main()et voyons :

Cat{name='Barsik', age=6}
Super, nous avons tout fait ! :) Voyons quelles autres possibilités nous avons...

Comment appeler la méthode d'un objet par son nom

Changeons légèrement la situation par rapport à l'exemple précédent. Disons que le développeur de la classe Cata fait une erreur avec les champs - les deux sont disponibles, il y a des getters et des setters pour eux, tout va bien. Le problème est différent : il a fait du private une méthode dont nous avons absolument besoin :
private void sayMeow() {

   System.out.println("Meow!");
}
En conséquence, nous créerons des objets Catdans notre programme, mais nous ne pourrons pas appeler leur méthode sayMeow(). Aurons-nous des chats qui ne miaulent pas ? Assez étrange :/ Comment puis-je résoudre ce problème ? Une fois de plus, l’API Reflection vient à la rescousse ! Nous connaissons le nom de la méthode requise. Le reste est une question de technique :
import learn.javarush.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("Barsik", 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 agissons à peu près de la même manière que dans le cas de l'accès à un champ privé. Nous obtenons d’abord la méthode dont nous avons besoin, qui est encapsulée dans un objet de classeMethod :
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
Avec de l’aide, getDeclaredMethod()vous pouvez « contacter » des méthodes privées. Ensuite, nous rendons la méthode appelable :
sayMeow.setAccessible(true);
Et enfin, on appelle la méthode sur l'objet souhaité :
sayMeow.invoke(cat);
L'appel d'une méthode ressemble aussi à un « appel à l'envers » : on a l'habitude de pointer un objet vers la méthode souhaitée à l'aide d'un point ( cat.sayMeow()), et lorsqu'on travaille avec la réflexion, on passe à la méthode l'objet à partir duquel il doit être appelé . Qu'avons-nous dans la console ?

Meow!
Tout s'est bien passé ! :) Vous voyez maintenant les possibilités étendues que nous offre le mécanisme de réflexion en Java. Dans des situations difficiles et inattendues (comme dans les exemples avec une classe d'une bibliothèque fermée), cela peut vraiment beaucoup nous aider. Cependant, comme toute grande puissance, cela implique également de grandes responsabilités. Les inconvénients de la réflexion sont décrits dans une section spéciale du site Web d'Oracle. Il y a trois inconvénients principaux :
  1. La productivité diminue. Les méthodes appelées par réflexion ont des performances inférieures à celles appelées normalement.

  2. Il existe des restrictions de sécurité. Le mécanisme de réflexion vous permet de modifier le comportement du programme pendant l'exécution. Mais dans votre environnement de travail sur un projet réel, il peut y avoir des restrictions qui ne vous permettent pas de le faire.

  3. Risque de divulgation d’informations privilégiées. Il est important de comprendre que l'utilisation de la réflexion viole directement le principe d'encapsulation : elle permet d'accéder à des champs privés, des méthodes, etc. Je pense qu'il n'est pas nécessaire d'expliquer qu'une violation directe et flagrante des principes de la POO ne devrait être utilisée que dans les cas les plus extrêmes, lorsqu'il n'existe aucun autre moyen de résoudre le problème pour des raisons indépendantes de votre volonté.

Utilisez le mécanisme de réflexion à bon escient et uniquement dans les situations où il ne peut être évité, et n'oubliez pas ses défauts. Ceci conclut notre conférence ! Cela s'est avéré assez volumineux, mais aujourd'hui vous avez appris beaucoup de nouvelles choses :)
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION