A quoi sert l'API Reflection ?

Le mécanisme de réflexion de Java permet à un développeur d'apporter des modifications et d'obtenir des informations sur les classes, les interfaces, les champs et les méthodes au moment de l'exécution sans connaître leurs noms.

L'API Reflection vous permet également de créer de nouveaux objets, d'appeler des méthodes et d'obtenir ou de définir des valeurs de champ.

Faisons une liste de tout ce que vous pouvez faire en utilisant la réflexion :

  • Identifier/déterminer la classe d'un objet
  • Obtenir des informations sur les modificateurs de classe, les champs, les méthodes, les constantes, les constructeurs et les superclasses
  • Découvrez quelles méthodes appartiennent aux interfaces implémentées
  • Créer une instance d'une classe dont le nom de classe n'est pas connu jusqu'à ce que le programme soit exécuté
  • Obtenir et définir la valeur d'un champ d'instance par nom
  • Appeler une méthode d'instance par son nom

Presque toutes les technologies Java modernes utilisent la réflexion. Il sous-tend la plupart des frameworks et bibliothèques Java / Java EE actuels, par exemple :

  • Frameworks Spring pour créer des applications Web
  • le cadre de test JUnit

Si vous n'avez jamais rencontré ces mécanismes auparavant, vous vous demandez probablement pourquoi tout cela est nécessaire. La réponse est assez simple mais aussi très vague : la réflexion augmente considérablement la flexibilité et la possibilité de personnaliser notre application et notre code.

Mais il y a toujours du pour et du contre. Mentionnons donc quelques inconvénients :

  • Violations de la sécurité des applications. La réflexion nous permet d'accéder à du code auquel nous ne devrions pas accéder (violation de l'encapsulation).
  • Contraintes de sécurité. Reflection requiert des autorisations d'exécution qui ne sont pas disponibles pour les systèmes exécutant un gestionnaire de sécurité.
  • Faible niveau de rendement. La réflexion en Java détermine les types dynamiquement en analysant le chemin de classe pour trouver la classe à charger. Cela réduit les performances du programme.
  • Difficile à entretenir. Le code qui utilise la réflexion est difficile à lire et à déboguer. Il est moins flexible et plus difficile à entretenir.

Travailler avec des classes à l'aide de l'API Reflection

Toutes les opérations de réflexion commencent par un objet java.lang.Class . Pour chaque type d'objet, une instance immuable de java.lang.Class est créée. Il fournit des méthodes pour obtenir les propriétés des objets, créer de nouveaux objets et appeler des méthodes.

Regardons la liste des méthodes de base pour travailler avec java.lang.Class :

Méthode Action
Chaîne getName(); Renvoie le nom de la classe
int getModifiers(); Renvoie les modificateurs d'accès
Paquet getPackage(); Renvoie des informations sur un paquet
Classe getSuperclass(); Renvoie des informations sur une classe parent
Classe[] getInterfaces(); Retourne un tableau d'interfaces
Constructeur[] getConstructors(); Renvoie des informations sur les constructeurs de classe
Champs[] getFields(); Renvoie les champs d'une classe
Champ getField(String fieldName); Renvoie un champ spécifique d'une classe par son nom
Méthode[] getMethods(); Retourne un tableau de méthodes

Ce sont les méthodes les plus importantes pour obtenir des données sur les classes, les interfaces, les champs et les méthodes. Il existe également des méthodes qui vous permettent d'obtenir ou de définir des valeurs de champ et d'accéder à des champs privés . Nous les verrons un peu plus tard.

Pour l'instant, nous allons parler de l'obtention de la classe java.lang.Class elle-même. Nous avons trois façons de le faire.

1. Utilisation de Class.forName

Dans une application en cours d'exécution, vous devez utiliser la méthode forName(String className) pour obtenir une classe.

Ce code montre comment nous pouvons créer des classes en utilisant la réflexion. Créons une classe Person avec laquelle nous pouvons travailler :


package com.company;

public class Person {
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

Et la deuxième partie de notre exemple est le code qui utilise la réflexion :


public class TestReflection {
    public static void main(String[] args) {
        try {
            Class<?> aClass = Class.forName("com.company.Person");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Cette approche est possible si le nom complet de la classe est connu. Ensuite, vous pouvez obtenir la classe correspondante en utilisant la méthode statique Class.forName() . Cette méthode ne peut pas être utilisée pour les types primitifs.

2. Utiliser .class

Si un type est disponible mais qu'il n'y a pas d'instance de celui-ci, vous pouvez obtenir la classe en ajoutant .class au nom du type. C'est le moyen le plus simple d'obtenir la classe d'un type primitif.


Class aClass = Person.class;

3. Utiliser .getClass()

Si un objet est disponible, le moyen le plus simple d'obtenir une classe est d'appeler object.getClass() .


Person person = new Person();
Class aClass = person.getClass();

Quelle est la différence entre les deux dernières approches ?

Utilisez A.class si vous savez quel objet de classe vous intéresse au moment du codage. Si aucune instance n'est disponible, vous devez utiliser .class .

Obtenir les méthodes d'une classe

Regardons les méthodes qui renvoient les méthodes de notre classe : getDeclaredMethods() et getMethods() .

getDeclaredMethods() renvoie un tableau qui contient des objets Method pour toutes les méthodes déclarées de la classe ou de l'interface représentée par l'objet Class, y compris les méthodes publiques, privées, par défaut et protégées, mais pas les méthodes héritées.

getMethods() renvoie un tableau contenant des objets Method pour toutes les méthodes publiques de la classe ou de l'interface représentée par l'objet Class — celles déclarées par la classe ou l'interface, ainsi que celles héritées des superclasses et des superinterfaces.

Voyons comment chacun d'eux fonctionne.

Commençons par getDeclaredMethods() . Pour nous aider à nouveau à comprendre la différence entre les deux méthodes, nous travaillerons ci-dessous avec la classe abstraite Numbers . Écrivons une méthode statique qui convertira notre tableau Method en List<String> :


import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestReflection {
    public static void main(String[] args) {
        final Method[] declaredMethods = Number.class.getDeclaredMethods();
        List<String> actualMethodNames = getMethodNames(declaredMethods);
        actualMethodNames.forEach(System.out::println);
    }

    private static List<String> getMethodNames(Method[] methods) {
        return Arrays.stream(methods)
                .map(Method::getName)
                .collect(Collectors.toList());
    }
}

Voici le résultat de l'exécution de ce code :

byteValue
shortValue
intValue
longValue
float floatValue ;
valeurdouble

Ce sont les méthodes déclarées à l'intérieur de la classe Number . Que renvoie getMethods() ? Modifions deux lignes dans l'exemple :


final Method[] methods = Number.class.getMethods();
List<String> actualMethodNames = getMethodNames(methods);

Ce faisant, nous verrons l'ensemble de méthodes suivant:

byteValue
shortValue
intValue
longValue
float floatValue ;
doubleValue
attendre
attendre
attendre
est égal à
toString
hashCode
getClass
notifier
notifyAll

Étant donné que toutes les classes héritent de Object , notre méthode renvoie également les méthodes publiques de la classe Object .

Obtenir les champs d'une classe

Les méthodes getFields et getDeclaredFields sont utilisées pour obtenir les champs d'une classe. A titre d'exemple, regardons la classe LocalDateTime . Nous allons réécrire notre code :


import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestReflection {
    public static void main(String[] args) {
        final Field[] declaredFields = LocalDateTime.class.getDeclaredFields();
        List<String> actualFieldNames = getFieldNames(declaredFields);
        actualFieldNames.forEach(System.out::println);
    }

    private static List<String> getFieldNames(Field[] fields) {
        return Arrays.stream(fields)
                .map(Field::getName)
                .collect(Collectors.toList());
    }
}

À la suite de l'exécution de ce code, nous obtenons l'ensemble des champs contenus dans la classe LocalDateTime.

MIN
MAX
serialVersionUID
date
heure

Par analogie avec notre précédent examen des méthodes, voyons ce qui se passe si on change un peu le code :


final Field[] fields = LocalDateTime.class.getFields();
List<String> actualFieldNames = getFieldNames(fields);

Sortir:

MIN
MAX

Voyons maintenant la différence entre ces méthodes.

La méthode getDeclaredFields renvoie un tableau d' objets Field pour tous les champs déclarés par la classe ou l'interface représentée par thisClasseobjet.

La méthode getFields renvoie un tableau d' objets Field pour tous les champs publics de la classe ou de l'interface représentée par leClasseobjet.

Regardons maintenant à l'intérieur de LocalDateTime .

Celui de la classeMINetMAXles champs sont publics, ce qui signifie qu'ils seront visibles via la méthode getFields . En revanche, ledate,temps,serialVersionUIDLes méthodes ont le modificateur privé , ce qui signifie qu'elles ne seront pas visibles via la méthode getFields , mais nous pouvons les obtenir en utilisant getDeclaredFields . C'est ainsi que nous pouvons accéder aux objets Field pour les champs privés .

Descriptions d'autres méthodes

Il est maintenant temps de parler de certaines méthodes de la classe Class , à savoir :

Méthode Action
getModifiers Obtenir les modificateurs pour notre classe
getPackage Obtenir le package qui contient notre classe
getSuperclass Obtenir la classe parent
getInterfaces Obtenir un tableau d'interfaces implémentées par une classe
obtenirNom Obtenir le nom complet de la classe
getSimpleName Obtenir le nom d'une classe

getModifiers()

Les modificateurs sont accessibles à l'aide d'unClasseobjet.

Les modificateurs sont des mots-clés comme public , static , interface , etc. Nous obtenons des modificateurs en utilisant la méthode getModifiers() :


Class<Person> personClass = Person.class;
int classModifiers = personClass.getModifiers();

Ce code définit la valeur d'unentiervariable qui est un champ de bits. Chaque modificateur d'accès peut être activé ou désactivé en définissant ou en effaçant le bit correspondant. Nous pouvons vérifier les modificateurs en utilisant les méthodes de la classe java.lang.reflect.Modifier :


import com.company.Person;
import java.lang.reflect.Modifier;

public class TestReflection {
    public static void main(String[] args) {
        Class<Person> personClass = Person.class;
        int classModifiers = personClass.getModifiers();

        boolean isPublic = Modifier.isPublic(classModifiers);
        boolean isStatic = Modifier.isStatic(classModifiers);
        boolean isFinal = Modifier.isFinal(classModifiers);
        boolean isAbstract = Modifier.isAbstract(classModifiers);
        boolean isInterface = Modifier.isInterface(classModifiers);

        System.out.printf("Class modifiers: %d%n", classModifiers);
        System.out.printf("Is public: %b%n", isPublic);
        System.out.printf("Is static: %b%n", isStatic);
        System.out.printf("Is final: %b%n", isFinal);
        System.out.printf("Is abstract: %b%n", isAbstract);
        System.out.printf("Is interface: %b%n", isInterface);
    }
}

Rappelez-vous à quoi ressemble la déclaration de notre Personne :


public class Person {
   …
}

Nous obtenons la sortie suivante :

Modificateurs de classe : 1
Est public : vrai
Est statique : faux
Est final : faux
Est abstrait : faux
Est interface : faux

Si nous rendons notre classe abstraite, alors nous avons :


public abstract class Person { … }

et cette sortie :

Modificateurs de classe : 1025
Est public : vrai
Est statique : faux
Est final : faux
Est abstrait : vrai
Est interface : faux

Nous avons modifié le modificateur d'accès, ce qui signifie que nous avons également modifié les données renvoyées via les méthodes statiques de la classe Modifier .

getPackage()

Connaissant uniquement une classe, nous pouvons obtenir des informations sur son package :


Class<Person> personClass = Person.class;
final Package aPackage = personClass.getPackage();
System.out.println(aPackage.getName());

getSuperclass()

Si nous avons un objet Class, nous pouvons accéder à sa classe parent :


public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    final Class<? super Person> superclass = personClass.getSuperclass();
    System.out.println(superclass);
}

Nous obtenons la classe Object bien connue :


class java.lang.Object

Mais si notre classe a une autre classe parente, nous la verrons à la place :


package com.company;

class Human {
    // Some info
}

public class Person extends Human {
    private int age;
    private String name;

    // Some info
}

Ici, nous obtenons notre classe parent:


class com.company.Human

getInterfaces()

Voici comment obtenir la liste des interfaces implémentées par la classe :


public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    final Class<?>[] interfaces = personClass.getInterfaces();
    System.out.println(Arrays.toString(interfaces));
}

Et n'oublions pas de changer notre classe Person :


public class Person implements Serializable { … }

Sortir:

[interface java.io.Serializable]

Une classe peut implémenter de nombreuses interfaces. C'est pourquoi nous obtenons un tableau deClasseobjets. Dans l'API Java Reflection, les interfaces sont également représentées parClasseobjets.

Remarque : la méthode ne renvoie que les interfaces implémentées par la classe spécifiée, pas sa classe parent. Pour obtenir une liste complète des interfaces implémentées par la classe, vous devez vous référer à la fois à la classe actuelle et à tous ses ancêtres dans la chaîne d'héritage.

getName() & getSimpleName() & getCanonicalName()

Écrivons un exemple impliquant une primitive, une classe imbriquée, une classe anonyme et la classe String :


public class TestReflection {
    public static void main(String[] args) {
        printNamesForClass(int.class, "int class (primitive)");
        printNamesForClass(String.class, "String.class (ordinary class)");
        printNamesForClass(java.util.HashMap.SimpleEntry.class,
                "java.util.HashMap.SimpleEntry.class (nested class)");
        printNamesForClass(new java.io.Serializable() {
                }.getClass(),
                "new java.io.Serializable(){}.getClass() (anonymous inner class)");
    }

    private static void printNamesForClass(final Class<?> clazz, final String label) {
        System.out.printf("%s:%n", label);
        System.out.printf("\tgetName()):\t%s%n", clazz.getName());
        System.out.printf("\tgetCanonicalName()):\t%s%n", clazz.getCanonicalName());
        System.out.printf("\tgetSimpleName()):\t%s%n", clazz.getSimpleName());
        System.out.printf("\tgetTypeName():\t%s%n%n", clazz.getTypeName());
    }
}

Résultat de notre programme :

int classe (primitive) :
getName() : int
getCanonicalName() : int
getSimpleName() : int
getTypeName() : int

String.class (classe ordinaire) :
getName()) : java.lang.String
getCanonicalName() ): java.lang.String
getSimpleName()): Chaîne
getTypeName(): java.lang.String

java.util.HashMap.SimpleEntry.class (classe imbriquée):
getName()): java.util.AbstractMap$SimpleEntry
getCanonicalName( )) : java.util.AbstractMap.SimpleEntry
getSimpleName()) : SimpleEntry
getTypeName() : java.util.AbstractMap$SimpleEntry

new java.io.Serializable(){}.getClass() (classe interne anonyme) :
getName() ): TestReflection$1
getCanonicalName()): null
getSimpleName()):
getTypeName(): TestReflection$1

Analysons maintenant la sortie de notre programme :

  • getName() renvoie le nom de l'entité.

  • getCanonicalName() renvoie le nom canonique de la classe de base, tel que défini par la spécification du langage Java. Renvoie null si la classe de base n'a pas de nom canonique (c'est-à-dire s'il s'agit d'une classe locale ou anonyme ou d'un tableau dont le type d'élément n'a pas de nom canonique).

  • getSimpleName() renvoie le nom simple de la classe de base tel qu'il est spécifié dans le code source. Renvoie une chaîne vide si la classe de base est anonyme.

  • getTypeName() renvoie une chaîne informative pour le nom de ce type.