Reflektion bedeutet grundsätzlich, den Code Ihres Programms als Daten zu verwenden.
Daher ist die Verwendung von Reflection möglicherweise eine gute Idee, wenn der Code Ihres Programms eine nützliche Datenquelle ist. (Aber es gibt Kompromisse, daher ist es möglicherweise nicht immer eine gute Idee.)
Stellen Sie sich zum Beispiel eine einfache Klasse vor:
public class Foo {
public int value;
public string anotherValue;
}
und Sie möchten XML daraus generieren. Sie könnten Code schreiben, um das XML zu generieren:
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;
}
Dies ist jedoch eine Menge Boilerplate-Code, und jedes Mal, wenn Sie die Klasse ändern, müssen Sie den Code aktualisieren. Wirklich, Sie könnten beschreiben, wie dieser Code funktioniert
- Erstellen Sie ein XML-Element mit dem Namen der Klasse
- für jede Eigenschaft der Klasse
- Erstellen Sie ein XML-Element mit dem Namen der Eigenschaft
- Fügen Sie den Wert der Eigenschaft in das XML-Element ein
- Fügen Sie das XML-Element zum Stammverzeichnis hinzu
Dies ist ein Algorithmus, und die Eingabe des Algorithmus ist die Klasse: Wir benötigen seinen Namen und die Namen, Typen und Werte seiner Eigenschaften. Hier kommt die Reflexion ins Spiel: Sie verschafft Ihnen Zugang zu diesen Informationen. Mit Java können Sie Typen mit den Methoden der Class
Klasse untersuchen.
Einige weitere Anwendungsfälle:
- Definieren Sie URLs in einem Webserver basierend auf den Methodennamen einer Klasse und URL-Parameter basierend auf den Methodenargumenten
- Konvertieren Sie die Struktur einer Klasse in eine GraphQL-Typdefinition
- Rufen Sie jede Methode einer Klasse auf, deren Name mit "test" als Unit-Testfall beginnt
Vollständige Reflexion bedeutet jedoch nicht nur, vorhandenen Code (der an sich als "Introspektion" bezeichnet wird) zu betrachten, sondern auch Code zu ändern oder zu generieren. Hierfür gibt es in Java zwei wichtige Anwendungsfälle: Proxies und Mocks.
Angenommen, Sie haben eine Schnittstelle:
public interface Froobnicator {
void froobnicateFruits(List<Fruit> fruits);
void froobnicateFuel(Fuel fuel);
// lots of other things to froobnicate
}
und Sie haben eine Implementierung, die etwas Interessantes bewirkt:
public class PowerFroobnicator implements Froobnicator {
// awesome implementations
}
Und tatsächlich haben Sie auch eine zweite Implementierung:
public class EnergySaverFroobnicator implements Froobnicator {
// efficient implementations
}
Jetzt möchten Sie auch eine Protokollausgabe; Sie möchten einfach eine Protokollnachricht, wenn eine Methode aufgerufen wird. Sie könnten jeder Methode explizit eine Protokollausgabe hinzufügen, aber das wäre ärgerlich, und Sie müssten es zweimal tun. einmal für jede Implementierung. (Umso mehr, wenn Sie weitere Implementierungen hinzufügen.)
Stattdessen können Sie einen Proxy schreiben:
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
}
Wieder gibt es jedoch ein sich wiederholendes Muster, das durch einen Algorithmus beschrieben werden kann:
- Ein Logger-Proxy ist eine Klasse, die eine Schnittstelle implementiert
- Es hat einen Konstruktor, der eine andere Implementierung der Schnittstelle übernimmt, und einen Logger
- für jede Methode in der Schnittstelle
- Die Implementierung protokolliert die Meldung "$ methodname called".
- und ruft dann dieselbe Methode auf der inneren Schnittstelle auf, wobei alle Argumente übergeben werden
und die Eingabe dieses Algorithmus ist die Schnittstellendefinition.
Mit Reflection können Sie mit diesem Algorithmus eine neue Klasse definieren. Mit Java können Sie dies mit den Methoden der java.lang.reflect.Proxy
Klasse tun , und es gibt Bibliotheken, die Ihnen noch mehr Leistung bieten.
Was sind die Nachteile der Reflexion?
- Ihr Code wird schwerer zu verstehen. Sie sind eine Abstraktionsebene, die weiter von den konkreten Auswirkungen Ihres Codes entfernt ist.
- Ihr Code ist schwerer zu debuggen. Insbesondere bei Code-generierenden Bibliotheken ist der ausgeführte Code möglicherweise nicht der von Ihnen geschriebene Code, aber der von Ihnen generierte Code, und der Debugger kann Ihnen diesen Code möglicherweise nicht anzeigen (oder Sie können Haltepunkte setzen).
- Ihr Code wird langsamer. Das dynamische Lesen von Typinformationen und der Zugriff auf Felder über ihre Laufzeit-Handles anstelle eines hartcodierten Zugriffs ist langsamer. Die dynamische Codegenerierung kann diesen Effekt abschwächen, was das Debuggen noch schwieriger macht.
- Ihr Code wird möglicherweise anfälliger. Der dynamische Reflection-Zugriff wird vom Compiler nicht typgeprüft, sondern löst zur Laufzeit Fehler aus.