Einführung in Java Reflection
Was Reflection in Java ist, wann man es einsetzt und ein Überblick über das java.lang.reflect-Paket.
Reflection ist die Fähigkeit eines Programms, seine eigene Struktur zur Laufzeit zu untersuchen und zu verändern: eine Klasse nach ihren Feldern, Methoden und Konstruktoren zu befragen, diese Felder zu lesen und zu schreiben, Methoden aufzurufen und neue Instanzen zu erstellen — alles ohne die Typen zur Compilezeit zu benennen. Während gewöhnliches Java statisch ist (der Compiler kennt jeden verwendeten Typ), ist Reflection dynamisch (Typen werden aus Strings, Konfigurationsdateien oder zur Laufzeit geladenen Klassen ermittelt). Dieser Teil des Buchs ist eine Tour durch java.lang.reflect; dieses Kapitel legt den Grundstein.
Was Reflection ermöglicht
Normaler Code benennt den Typ, mit dem er arbeitet:
User u = new User("ada");
String name = u.getName();Reflektiver Code erreicht dasselbe Ergebnis, ohne User oder getName als Compile-Zeit-Token zu schreiben — sie sind Strings, die zur Laufzeit aufgelöst werden:
Class<?> cls = Class.forName("com.example.User");
Object u = cls.getDeclaredConstructor(String.class).newInstance("ada");
Object name = cls.getMethod("getName").invoke(u);Die zweite Form ist weitaus ausführlicher und langsamer, und sie verzichtet auf die Typsicherheit zur Compilezeit. Man würde sie niemals für gewöhnliche Anwendungslogik verwenden. Man greift genau dann darauf zurück, wenn der Typ erst zur Laufzeit bekannt ist.
Wann Reflection sinnvoll ist
Reflection ist das Herzstück von Frameworks, nicht von alltäglichem Code. Typische Einsatzbereiche:
- Dependency Injection (Spring, Guice, CDI): Der Container liest Annotationen und Konstruktorsignaturen, instanziiert dann Beans und verdrahtet sie, ohne je deren Quellcode gesehen zu haben.
- Serialisierung (Jackson, Gson): Eine JSON-Bibliothek durchläuft die Felder eines Objekts, um sie zu lesen oder zu befüllen, ohne dass man für jede Klasse Mapping-Code schreiben muss.
- ORMs (Hibernate, JPA): Spalten werden Feldern zugeordnet, indem die Entity-Klasse per Reflection untersucht wird.
- Test-Runner (JUnit): Methoden mit
@Testwerden gefunden und aufgerufen. - Plugin-Systeme: Eine in einer Konfigurationsdatei genannte Klasse wird geladen, und eine bekannte Interface-Methode wird auf ihr aufgerufen.
Der gemeinsame Nenner: ein allgemeiner Mechanismus, der über beliebige Benutzertypen operiert, gegen die er nicht kompiliert wurde. Genau das ist das Problem, das Reflection löst.
Die zentralen Typen in java.lang.reflect
Reflection beginnt mit einem Class-Objekt (behandelt im nächsten Kapitel, Java Class-Objekte) und verzweigt sich in eine kleine Familie von Klassen, die jeweils eine Art von Member beschreiben:
| Typ | Repräsentiert | Aus Class ermittelt via |
|---|---|---|
Field | ein Feld | getField / getDeclaredField(s) |
Method | eine Methode | getMethod / getDeclaredMethod(s) |
Constructor<T> | einen Konstruktor | getConstructor / getDeclaredConstructor(s) |
Parameter | einen Methoden-/Konstruktorparameter | Executable.getParameters() |
Modifier | ein Hilfsobjekt für den int-Modifier-Bitset | statische Methoden |
Field, Method und Constructor teilen eine gemeinsame Superklassen-Hierarchie: Member (das Interface) und AccessibleObject (das setAccessible bereitstellt). Diese gemeinsame Basis ist der Grund, warum "zugänglich machen" und "Annotationen lesen" identisch aussehen, egal welches Member man hält.
Die Unterscheidung zwischen get… und getDeclared…
Fast jede Suchmethode existiert in zwei Varianten, und der Unterschied ist in jedem späteren Kapitel relevant:
getField/getMethod/getConstructor— gibt public-Member zurück, einschließlich vererbter von Superklassen und Interfaces.getDeclaredField/getDeclaredMethod/getDeclaredConstructor— gibt Member beliebiger Zugriffsebene zurück (private,protected, package), aber nur jene, die in dieser exakten Klasse deklariert sind — keine geerbten.
getMethods() sieht also eine von einer Elternklasse geerbte public-Methode, aber keine private-Hilfsmethode der Klasse selbst; getDeclaredMethods() sieht die private-Hilfsmethode, aber nicht die geerbte public-Methode. Um auf ein privates geerbtes Member zuzugreifen, muss man mit getSuperclass() nach oben gehen und getDeclared… auf jeder Ebene aufrufen.
Die Kosten: Geschwindigkeit, Sicherheit und Kapselung
Reflection ist mächtig, und jede Macht hat ihren Preis.
- Performance. Reflektive Aufrufe sind langsamer als direkte Aufrufe — Methoden-/Feldsuchen, Zugriffsüberprüfungen und Argument-Boxing erzeugen Overhead. Der JIT optimiert häufig verwendete reflektive Aufrufe gut, aber Reflection in einer engen Schleife ist ein Warnsignal. Man sollte die
Method-/Field-Objekte cachen und sie niemals pro Aufruf neu suchen. - Keine Compilezeit-Sicherheit. Ein Tippfehler in einem Methodennamen kompiliert einwandfrei und schlägt erst zur Laufzeit mit
NoSuchMethodExceptionfehl. Refactoring-Tools benennengetNameüberall um — außer innerhalb des Strings"getName". - Kapselung wird durchbrochen.
setAccessible(true)erlaubt das Lesen und Schreiben vonprivate-Zustand. So befüllen Serializer Felder ohne Setter, aber man koppelt sich damit an interne Details, deren Stabilität der Autor der Klasse nie versprochen hat. - Modul-Einschränkungen. Seit Java 9 kann das Modulsystem reflektiven Zugriff auf nicht exportierte Pakete verweigern. Der Aufruf von
setAccessible(true)über eine Modulgrenze hinweg, die das Paket nicht peropensfreigibt, wirftInaccessibleObjectException.
Ein ausgearbeitetes Beispiel: ein kleiner generischer Objekt-Dumper
Um den Anwendungsbereich konkret zu machen, hier eine einzige reflektive Routine, die die Felder und deren Werte beliebiger Objekte ausgibt — die Art von Funktionalität, die ein Debugger oder eine Logging-Bibliothek einsetzt. Sie benennt keinen Anwendungstyp; sie funktioniert mit allem, was übergeben wird.
Was man aus der Ausführung mitnehmen sollte:
- Die
dump-Methode hat keinen konkreten Typ benannt und dennoch sowohlPointals auchUserausgegeben. Ihr einziger Vertrag lautet: "Gib mir einObject"; alles über die Struktur — Feldnamen, Typen, Werte — kam zur Laufzeit vongetClass(). Das ist der grundlegende Zug der Reflection: eine Routine, beliebige Eingaben. getDeclaredFields()hat alle Felder zurückgegeben, einschließlich derprivate-Felder, aber zum Lesen war vorhersetAccessible(true)erforderlich. Ohne diesen Aufruf wirftf.get(obj)bei einem privaten FeldIllegalAccessException. Die Suche und der Zugriff sind zwei separate Hürden.Modifier.toString(f.getModifiers())hat den rohen Modifier-Bitset in lesbaren Text wieprivate finalumgewandelt. Modifier werden alsintvon Flag-Bits gespeichert; derModifier-Helfer dekodiert sie, sodass man die Bits nicht manuell prüfen muss.- Das dritte Objekt wurde ohne ein einziges
new User(...)im Quellcode erstellt —Class.forName("…$User")hat die verschachtelte Klasse aus einem String aufgelöst (beachte das$-Trennzeichen für verschachtelte Typen), undgetDeclaredConstructor(...).newInstance(...)hat sie instanziiert. Das ist das Plugin-Lade-Muster im Miniaturformat: ein Name rein, ein Objekt raus. - Das Lesen des Feld-Werts (
f.get(obj)) und das Lesen der Feld-Metadaten (f.getName(),f.getModifiers()) sind unabhängig voneinander. Metadaten benötigen keine Instanz und keine Zugänglichkeit; Werte benötigen das Objekt und, bei privaten Feldern, das Zugänglichkeits-Flag.
Wie der Rest dieses Teils aufgebaut ist
Jedes weitere Kapitel beleuchtet eine Ecke im Detail:
- Class-Objekte — die drei Wege, ein
Class<T>zu erhalten und was es verrät. - Felder — Felder untersuchen, lesen und schreiben (einschließlich
privateundfinal). - Methoden — Methoden finden und aufrufen, Überladungsauflösung, Rückgabewerte.
- Konstruktoren — Instanzen per Reflection erstellen, einschließlich privater Konstruktoren.
- Annotationen — die Metadaten lesen, die mit Annotationen hinzugefügt wurden, zur Laufzeit.
- Dynamische Proxys — vollständige Interface-Implementierungen zur Laufzeit synthetisieren.
Dabei sollte man den Trade-off immer im Blick behalten: Reflection ist das richtige Werkzeug, wenn der Typ zur Laufzeit wirklich nicht bekannt ist, und das falsche Werkzeug, wenn er es ist. Das nächste Kapitel beginnt an der Wurzel von allem — dem Class-Objekt.