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 Class
classe.
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.Proxy
classe, 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.