CodeGym /Blog Java /Random-FR /API de réflexion : réflexion. Le côté obscur de Java
Auteur
Pavlo Plynko
Java Developer at CodeGym

API de réflexion : réflexion. Le côté obscur de Java

Publié dans le groupe Random-FR
Salutations, jeune Padawan. Dans cet article, je vais vous parler de la Force, un pouvoir que les programmeurs Java n'utilisent que dans des situations apparemment impossibles. Le côté obscur de Java est l'API Reflection. En Java, la réflexion est implémentée à l'aide de l'API Java Reflection.

Qu'est-ce que la réflexion Java ?

Il existe une définition courte, précise et populaire sur Internet. La réflexion ( du latin reflexio - revenir en arrière ) est un mécanisme permettant d'explorer les données d'un programme pendant son exécution. La réflexion vous permet d'explorer des informations sur les champs, les méthodes et les constructeurs de classe. La réflexion vous permet de travailler avec des types qui n'étaient pas présents au moment de la compilation, mais qui sont devenus disponibles au moment de l'exécution. La réflexion et un modèle logiquement cohérent pour l'émission d'informations d'erreur permettent de créer un code dynamique correct. En d'autres termes, comprendre comment fonctionne la réflexion en Java vous ouvrira un certain nombre d'opportunités incroyables. Vous pouvez littéralement jongler avec les classes et leurs composants. Voici une liste de base de ce que la réflexion permet :
  • Apprendre/déterminer la classe d'un objet ;
  • Obtenir des informations sur les modificateurs, les champs, les méthodes, les constantes, les constructeurs et les superclasses d'une classe ;
  • Découvrez quelles méthodes appartiennent aux interfaces implémentées ;
  • Créez une instance d'une classe dont le nom de classe est inconnu jusqu'à l'exécution ;
  • Obtenir et définir les valeurs des champs d'un objet par nom ;
  • Appelez la méthode d'un objet par son nom.
La réflexion est utilisée dans presque toutes les technologies Java modernes. Il est difficile d'imaginer que Java, en tant que plate-forme, aurait pu parvenir à une adoption aussi répandue sans réflexion. Très probablement, cela n'aurait pas été le cas. Maintenant que vous êtes généralement familiarisé avec la réflexion en tant que concept théorique, passons à son application pratique ! Nous n'apprendrons pas toutes les méthodes de l'API Reflection, uniquement celles que vous rencontrerez réellement dans la pratique. Puisque la réflexion implique de travailler avec des classes, nous allons commencer avec une classe simple appelée MyClass:

public class MyClass {
   private int number;
   private String name = "default";
//    public MyClass(int number, String name) {
//        this.number = number;
//        this.name = name;
//    }
   public int getNumber() {
       return number;
   }
   public void setNumber(int number) {
       this.number = number;
   }
   public void setName(String name) {
       this.name = name;
   }
   private void printData(){
       System.out.println(number + name);
   }
}
Comme vous pouvez le voir, c'est une classe très basique. Le constructeur avec paramètres est délibérément commenté. Nous y reviendrons plus tard. Si vous avez regardé attentivement le contenu de la classe, vous avez probablement remarqué l'absence de getter pour le champ name . Le champ de nom lui-même est marqué avec le modificateur d'accès privé : nous ne pouvons pas y accéder en dehors de la classe elle-même, ce qui signifie que nous ne pouvons pas récupérer sa valeur. « Alors quel est le problème ? vous dites. "Ajouter un getter ou modifier le modificateur d'accès". Et vous auriez raison, à moins queMyClassse trouvait dans une bibliothèque AAR compilée ou dans un autre module privé sans possibilité d'apporter des modifications. En pratique, cela arrive tout le temps. Et un programmeur négligent a tout simplement oublié d'écrire un getter . C'est le moment même de se souvenir de la réflexion ! Essayons d'accéder au champ de nom privé de la MyClassclasse :

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; // No getter =(
   System.out.println(number + name); // Output: 0null
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(number + name); // Output: 0default
}
Analysons ce qui vient de se passer. En Java, il existe une merveilleuse classe appelée Class. Il représente des classes et des interfaces dans une application Java exécutable. Nous n'aborderons pas la relation entre Classet ClassLoader, puisque ce n'est pas le sujet de cet article. Ensuite, pour récupérer les champs de cette classe, vous devez appeler la getFields()méthode. Cette méthode renverra tous les champs accessibles de cette classe. Cela ne fonctionne pas pour nous, car notre champ est private , nous utilisons donc la getDeclaredFields()méthode. Cette méthode renvoie également un tableau de champs de classe, mais elle inclut désormais des champs privés et protégés . Dans ce cas, nous connaissons le nom du champ qui nous intéresse, nous pouvons donc utiliser la getDeclaredField(String)méthode, oùStringest le nom du champ souhaité. Note: getFields()et getDeclaredFields()ne renvoie pas les champs d'une classe parente ! Super. Nous avons un Fieldobjet faisant référence à notre nom . Étant donné que le champ n'était pas public , nous devons accorder l'accès pour travailler avec lui. La setAccessible(true)méthode permet d'aller plus loin. Maintenant, le champ du nom est sous notre contrôle total ! Vous pouvez récupérer sa valeur en appelant la méthode Fieldde l'objet , où est une instance de notre classe. Nous convertissons le type en et attribuons la valeur à notre variable de nom . Si nous ne trouvons pas de setter pour définir une nouvelle valeur dans le champ name, vous pouvez utiliser la méthode set :get(Object)ObjectMyClassString

field.set(myClass, (String) "new value");
Toutes nos félicitations! Vous venez de maîtriser les bases de la réflexion et d'accéder à un champ privé ! Faites attention au try/catchbloc et aux types d'exceptions gérées. L'IDE vous dira que leur présence est requise par elle-même, mais vous pouvez clairement dire par leur nom pourquoi ils sont ici. Passons à autre chose ! Comme vous l'avez peut-être remarqué, notre MyClassclasse dispose déjà d'une méthode pour afficher des informations sur les données de la classe :

private void printData(){
       System.out.println(number + name);
   }
Mais ce programmeur a laissé ses empreintes digitales ici aussi. La méthode a un modificateur d'accès privé et nous devons écrire notre propre code pour afficher les données à chaque fois. Quel bordel. Où est passé notre reflet ? Écrivez la fonction suivante :

public static void printData(Object myClass){
   try {
       Method method = myClass.getClass().getDeclaredMethod("printData");
       method.setAccessible(true);
       method.invoke(myClass);
   } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
       e.printStackTrace();
   }
}
La procédure ici est à peu près la même que celle utilisée pour récupérer un champ. Nous accédons à la méthode souhaitée par son nom et lui accordons l'accès. Et sur l' Methodobjet, nous appelons la invoke(Object, Args)méthode, où Objectest également une instance de la MyClassclasse. Argssont les arguments de la méthode, bien que la nôtre n'en ait pas. Maintenant, nous utilisons la printDatafonction pour afficher des informations :

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // Output: 0default
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       field.set(myClass, (String) "new value");
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   printData(myClass);// Output: 0new value
}
Hourra! Nous avons maintenant accès à la méthode privée de la classe. Mais que se passe-t-il si la méthode a des arguments, et pourquoi le constructeur est-il commenté ? Tout en son temps. Il ressort clairement de la définition au début que la réflexion vous permet de créer des instances d'une classe au moment de l'exécution (pendant que le programme est en cours d'exécution) ! Nous pouvons créer un objet en utilisant le nom complet de la classe. Le nom complet de la classe est le nom de la classe, y compris le chemin de son package .
API de réflexion : réflexion.  Le côté obscur de Java - 2
Dans ma hiérarchie de packages , le nom complet de MyClass serait "reflection.MyClass". Il existe également un moyen simple d'apprendre le nom d'une classe (renvoyez le nom de la classe sous forme de chaîne):

MyClass.class.getName()
Utilisons la réflexion Java pour créer une instance de la classe :

public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       myClass = (MyClass) clazz.newInstance();
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(myClass); // Output: created object reflection.MyClass@60e53b93
}
Lorsqu'une application Java démarre, toutes les classes ne sont pas chargées dans la JVM. Si votre code ne fait pas référence à la MyClassclasse, alors ClassLoader, qui est responsable du chargement des classes dans la JVM, ne chargera jamais la classe. Cela signifie que vous devez forcer ClassLoaderpour le charger et obtenir une description de classe sous la forme d'une Classvariable. C'est pourquoi nous avons la forName(String)méthode, où Stringest le nom de la classe dont nous avons besoin de la description. Après avoir obtenu l' Сlassobjet, l'appel de la méthode newInstance()renverra un Objectobjet créé à l'aide de cette description. Il ne reste plus qu'à fournir cet objet à notreMyClassclasse. Cool! C'était difficile, mais compréhensible, j'espère. Nous pouvons maintenant créer une instance d'une classe en littéralement une seule ligne ! Malheureusement, l'approche décrite ne fonctionnera qu'avec le constructeur par défaut (sans paramètres). Comment appeler des méthodes et des constructeurs avec des paramètres ? Il est temps de décommenter notre constructeur. Comme prévu, newInstance()impossible de trouver le constructeur par défaut et ne fonctionne plus. Réécrivons l'instanciation de classe :

public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       Class[] params = {int.class, String.class};
       myClass = (MyClass) clazz.getConstructor(params).newInstance(1, "default2");
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);// Output: created object reflection.MyClass@60e53b93
}
La getConstructors()méthode doit être appelée sur la définition de classe pour obtenir des constructeurs de classe, puis getParameterTypes()doit être appelée pour obtenir les paramètres d'un constructeur :

Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
Cela nous donne tous les constructeurs et leurs paramètres. Dans mon exemple, je fais référence à un constructeur spécifique avec des paramètres spécifiques, déjà connus. Et pour appeler ce constructeur, on utilise la newInstanceméthode, à laquelle on passe les valeurs de ces paramètres. Il en sera de même lors de l'utilisation invokepour appeler des méthodes. Cela soulève la question suivante : quand l'appel de constructeurs par réflexion est-il utile ? Comme déjà mentionné au début, les technologies Java modernes ne peuvent pas se passer de l'API Java Reflection. Par exemple, Dependency Injection (DI), qui combine des annotations avec la réflexion de méthodes et de constructeurs pour former le populaire Darerbibliothèque pour le développement Android. Après avoir lu cet article, vous pouvez vous considérer en toute confiance comme formé à l'API Java Reflection. Ils n'appellent pas la réflexion le côté obscur de Java pour rien. Cela brise complètement le paradigme de la POO. En Java, l'encapsulation masque et restreint l'accès des autres à certains composants du programme. Lorsque nous utilisons le modificateur privé, nous souhaitons que ce champ ne soit accessible qu'à partir de la classe où il existe. Et nous construisons l'architecture ultérieure du programme sur la base de ce principe. Dans cet article, nous avons vu comment vous pouvez utiliser la réflexion pour vous frayer un chemin n'importe où. Le design pattern créationnel Singletonen est un bon exemple en tant que solution architecturale. L'idée de base est qu'une classe implémentant ce modèle n'aura qu'une seule instance lors de l'exécution de l'ensemble du programme. Ceci est accompli en ajoutant le modificateur d'accès privé au constructeur par défaut. Et ce serait très mauvais si un programmeur utilisait la réflexion pour créer plus d'instances de telles classes. Au fait, j'ai récemment entendu un collègue poser une question très intéressante : une classe qui implémente le modèle Singleton peut-elle être héritée ? Se pourrait-il que, dans ce cas, même la réflexion soit impuissante ? Laissez vos commentaires sur l'article et votre réponse dans les commentaires ci-dessous, et posez-y vos propres questions !

Plus de lecture :

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION