Pourquoi devrais-je utiliser la réflexion?

29

Je suis nouveau à Java; à travers mes études, j'ai lu que la réflexion est utilisée pour invoquer des classes et des méthodes, et savoir quelles méthodes sont implémentées ou non.

Quand dois-je utiliser la réflexion et quelle est la différence entre utiliser la réflexion et instancier des objets et appeler des méthodes de manière traditionnelle?

Hamzah khammash
la source
10
Veuillez faire votre part de recherche avant de poster. Il y a beaucoup de matériel sur StackExchange (comme l'a noté @Jalayn) et le Web en général sur la réflexion. Je vous suggère de lire par exemple le Tutoriel Java sur la réflexion et de revenir après cela si vous avez des questions plus concrètes.
Péter Török
1
Il doit y avoir un million de dupes.
DeadMG
3
Plus de quelques programmeurs professionnels répondraient «aussi rarement que possible, peut-être même jamais».
Ross Patterson le

Réponses:

38
  • La réflexion est beaucoup plus lente que d'appeler simplement des méthodes par leur nom, car elle doit inspecter les métadonnées dans le bytecode au lieu d'utiliser simplement des adresses et des constantes précompilées.

  • La réflexion est également plus puissante: vous pouvez récupérer la définition d'un membre protectedou final, supprimer la protection et la manipuler comme si elle avait été déclarée mutable! De toute évidence, cela sape la plupart des garanties que le langage offre normalement à vos programmes et peut être très, très dangereux.

Et cela explique à peu près quand l'utiliser. Normalement, non. Si vous voulez appeler une méthode, il suffit de l'appeler. Si vous voulez muter un membre, déclarez-le simplement mutable au lieu d'aller derrière le dos de la compilation.

Une utilisation utile de la réflexion dans le monde réel est lors de l'écriture d'un cadre qui doit interagir avec des classes définies par l'utilisateur, où l'auteur du cadre ne sait pas quels seront les membres (ou même les classes). La réflexion leur permet de traiter n'importe quelle classe sans le savoir à l'avance. Par exemple, je ne pense pas qu'il serait possible d'écrire une bibliothèque complexe orientée aspect sans réflexion.

Autre exemple, JUnit utilisait un peu de réflexion triviale: il énumère toutes les méthodes de votre classe, suppose que toutes celles qui testXXXsont appelées sont des méthodes de test et n'exécute que celles-ci. Mais cela peut maintenant être mieux fait avec des annotations à la place, et en fait JUnit 4 est largement passé aux annotations à la place.

Kilian Foth
la source
6
"Plus puissant" a besoin de soins. Vous n'avez pas besoin de réflexion pour obtenir l'intégralité de Turing, donc aucun calcul n'a jamais besoin de réflexion. Bien sûr, Turing complete ne dit rien sur les autres types de puissance, comme les capacités d'E / S et, bien sûr, la réflexion.
Steve314
1
La réflexion n'est pas nécessairement "beaucoup plus lente". Vous pouvez utiliser la réflexion une fois pour générer un bytecode de wrapper d'appel direct.
SK-logic
2
Vous le saurez quand vous en aurez besoin. Je me demandais souvent pourquoi (en dehors de la génération de la langue), on en aurait besoin. Puis, tout à coup, je l'ai fait ... J'ai dû monter et descendre les chaînes parent / enfant pour jeter un œil aux données lorsque j'ai reçu des panneaux d'autres développeurs pour qu'ils apparaissent dans l'un des systèmes que je maintiens.
Brian Knoblauch
@ SK-logic: en fait, pour générer du bytecode, vous n'avez pas du tout besoin de réflexion (en effet, la réflexion ne contient aucune API pour la manipulation de bytecode!).
Joachim Sauer
1
@JoachimSauer, bien sûr, mais vous aurez besoin d'une API de réflexion pour charger ce bytecode généré.
SK-logic
15

J'étais comme toi une fois, je ne connaissais pas grand-chose à la réflexion - toujours pas - mais je l'ai utilisée une fois.

J'avais une classe avec deux classes internes, et chaque classe avait beaucoup de méthodes.

J'avais besoin d'invoquer toutes les méthodes de la classe interne et les appeler manuellement aurait été trop de travail.

En utilisant la réflexion, j'ai pu invoquer toutes ces méthodes en seulement 2-3 lignes de code, au lieu du nombre de méthodes elles-mêmes.

Mahmoud Hossam
la source
4
Pourquoi le downvote?
Mahmoud Hossam
1
Vote pour le préambule
Dmitry Minkovsky
1
@MahmoudHossam Ce n'est peut-être pas une bonne pratique, mais votre réponse illustre une tactique importante que l'on peut déployer.
ankush981
13

Je regrouperais les utilisations de la réflexion en trois groupes:

  1. Instanciation de classes arbitraires. Par exemple, dans un framework d'injection de dépendances, vous déclarez probablement que l'interface ThingDoer est implémentée par la classe NetworkThingDoer. Le framework trouverait alors le constructeur de NetworkThingDoer et l'instancierait.
  2. Marshalling et démarshalling à un autre format. Par exemple, mapper un objet avec des getters et des paramètres qui suivent la convention du bean en JSON et vice-versa. Le code ne connaît pas réellement les noms des champs ou des méthodes, il examine juste la classe.
  3. Envelopper une classe dans une couche de redirection (peut-être que cette liste n'est pas réellement chargée, mais juste un pointeur vers quelque chose qui sait comment la récupérer dans la base de données) ou truquer une classe entièrement (jMock créera une classe synthétique qui implémente une interface à des fins de test).
Kevin Peterson
la source
C'est la meilleure explication de réflexion que j'ai trouvée sur StackExchange. La plupart des réponses répètent ce que dit le Java Trail (qui est "vous pouvez accéder à toutes ces propriétés", mais pas pourquoi vous le feriez), fournissent des exemples de choses avec réflexion qui sont beaucoup plus faciles à faire sans, ou donnent une réponse vague sur la façon dont Spring l'utilise. Cette réponse donne en fait trois exemples valides qui NE PEUVENT PAS être facilement contournés par JVM sans réflexion. Merci!
ndm13
3

La réflexion permet à un programme de travailler avec du code qui peut ne pas être présent et de le faire de manière fiable.

Le "code normal" contient des extraits de code URLConnection c = nullqui, par sa simple présence, font que le chargeur de classe charge la classe URLConnection dans le cadre du chargement de cette classe, lançant une exception ClassNotFound et quittant.

La réflexion vous permet de charger des classes en fonction de leurs noms sous forme de chaîne et de les tester pour diverses propriétés (utiles pour plusieurs versions hors de votre contrôle) avant de lancer des classes réelles qui en dépendent. Un exemple typique est le code spécifique à OS X utilisé pour rendre les programmes Java natifs sous OS X, qui ne sont pas présents sur d'autres plates-formes.


la source
2

Fondamentalement, la réflexion signifie utiliser le code de votre programme comme données.

Par conséquent, l'utilisation de la réflexion peut être une bonne idée lorsque le code de votre programme est une source utile de données. (Mais il y a des compromis, ce n'est donc pas toujours une bonne idée.)

Par exemple, considérons une classe simple:

public class Foo {
  public int value;
  public string anotherValue;
}

et vous voulez en générer du XML. Vous pouvez écrire du code pour générer le XML:

public XmlNode generateXml(Foo foo) {
  XmlElement root = new XmlElement("Foo");
  XmlElement valueElement = new XmlElement("value");
  valueElement.add(new XmlText(Integer.toString(foo.value)));
  root.add(valueElement);
  XmlElement anotherValueElement = new XmlElement("anotherValue");
  anotherValueElement.add(new XmlText(foo.anotherValue));
  root.add(anotherValueElement);
  return root;
}

Mais c'est beaucoup de code passe-partout, et chaque fois que vous changez la classe, vous devez mettre à jour le code. Vraiment, vous pourriez décrire ce que ce code fait

  • créer un élément XML avec le nom de la classe
  • pour chaque propriété de la classe
    • créer un élément XML avec le nom de la propriété
    • mettre la valeur de la propriété dans l'élément XML
    • ajouter l'élément XML à la racine

Il s'agit d'un algorithme, et l'entrée de l'algorithme est la classe: nous avons besoin de son nom, ainsi que des noms, types et valeurs de ses propriétés. C'est là qu'intervient la réflexion: elle vous donne accès à ces informations. Java vous permet d'inspecter les types à l'aide des méthodes de la Classclasse.

Quelques autres cas d'utilisation:

  • définir des URL dans un serveur Web en fonction des noms de méthode d'une classe et des paramètres d'URL en fonction des arguments de méthode
  • convertir la structure d'une classe en une définition de type GraphQL
  • appeler chaque méthode d'une classe dont le nom commence par "test" comme cas de test unitaire

Cependant, une réflexion complète signifie non seulement regarder le code existant (qui en soi est appelé "introspection"), mais aussi modifier ou générer du code. Il y a deux cas d'utilisation importants en Java pour cela: les proxys et les mocks.

Disons que vous avez une interface:

public interface Froobnicator {
  void froobnicateFruits(List<Fruit> fruits);
  void froobnicateFuel(Fuel fuel);
  // lots of other things to froobnicate
}

et vous avez une implémentation qui fait quelque chose d'intéressant:

public class PowerFroobnicator implements Froobnicator {
  // awesome implementations
}

Et en fait, vous avez également une deuxième implémentation:

public class EnergySaverFroobnicator implements Froobnicator {
  // efficient implementations
}

Maintenant, vous voulez également une sortie de journal; vous voulez simplement un message de journal chaque fois qu'une méthode est appelée. Vous pouvez ajouter explicitement la sortie du journal à chaque méthode, mais ce serait ennuyeux, et vous devriez le faire deux fois; une fois pour chaque implémentation. (Donc encore plus lorsque vous ajoutez plus d'implémentations.)

Au lieu de cela, vous pouvez écrire un proxy:

public class LoggingFroobnicator implements Froobnicator {
  private Logger logger;
  private Froobnicator inner;

  // constructor that sets those two

  public void froobnicateFruits(List<Fruit> fruits) {
    logger.logDebug("froobnicateFruits called");
    inner.froobnicateFruits(fruits);
  }

  public void froobnicateFuel(Fuel fuel) {
    logger.logDebug("froobnicateFuel( called");
    inner.froobnicateFuel(fuel);
  }
  // lots of other things to froobnicate
}

Encore une fois, cependant, il existe un modèle répétitif qui peut être décrit par un algorithme:

  • un proxy d'enregistreur est une classe qui implémente une interface
  • il a un constructeur qui prend une autre implémentation de l'interface et un enregistreur
  • pour chaque méthode de l'interface
    • l'implémentation enregistre un message "$ methodname called"
    • puis appelle la même méthode sur l'interface interne, en passant tous les arguments

et l'entrée de cet algorithme est la définition de l'interface.

La réflexion vous permet de définir une nouvelle classe à l'aide de cet algorithme. Java vous permet de le faire en utilisant les méthodes de la java.lang.reflect.Proxyclasse, et il existe des bibliothèques qui vous donnent encore plus de puissance.

Quels sont donc les inconvénients de la réflexion?

  • Votre code devient plus difficile à comprendre. Vous êtes un niveau d'abstraction encore plus éloigné des effets concrets de votre code.
  • Votre code devient plus difficile à déboguer. Surtout avec les bibliothèques générant du code, le code qui est exécuté peut ne pas être le code que vous avez écrit, mais le code que vous avez généré et le débogueur peuvent ne pas être en mesure de vous montrer ce code (ou de vous laisser placer des points d'arrêt).
  • Votre code devient plus lent. La lecture dynamique des informations de type et l'accès aux champs par leurs poignées d'exécution au lieu d'un accès codé en dur sont plus lents. La génération de code dynamique peut atténuer cet effet, au prix d'être encore plus difficile à déboguer.
  • Votre code peut devenir plus fragile. L'accès à la réflexion dynamique n'est pas vérifié par le compilateur, mais génère des erreurs lors de l'exécution.
Sebastian Redl
la source
1

Reflection peut automatiquement synchroniser certaines parties de votre programme, alors qu'auparavant, vous auriez dû mettre à jour manuellement votre programme pour utiliser les nouvelles interfaces.

DeadMG
la source
5
Le prix que vous payez dans ce cas est que vous perdez la vérification de type par le compilateur et la sécurité du refactor dans l'IDE. C'est un compromis que je ne suis pas prêt à faire.
Barend