Java Reflection: Felder untersuchen
Felder zur Laufzeit in Java mit der Reflection-API inspizieren, lesen und schreiben.
Ein Field-Objekt beschreibt ein Feld einer Klasse: seinen Namen, seinen Typ, seine Modifikatoren und — bei einer gegebenen Instanz — seinen Wert. Reflection ermöglicht es, die Felder einer Klasse aufzulisten, zu lesen und zu schreiben, auch wenn sie private sind und weder Getter noch Setter besitzen. Genau so befüllen JSON-Deserialisierer Objekte und ORMs hydratisieren Entitäten. Dieses Kapitel behandelt das Abrufen von Field-Objekten, das Lesen und Schreiben von Werten, das setAccessible-Gate und den Sonderfall von final-Feldern.
Dieses Kapitel baut auf der Reflection-Einführung auf. Für die begleitenden APIs siehe Methoden untersuchen und Konstruktoren untersuchen.
Field-Objekte abrufen
Die gleiche Aufteilung in public vs. declared aus der Einführung gilt hier ebenfalls:
Class<?> c = User.class;
Field f1 = c.getField("name"); // public field, incl. inherited — else NoSuchFieldException
Field f2 = c.getDeclaredField("name"); // any access level, this class only
Field[] all = c.getFields(); // public fields, incl. inherited
Field[] mine = c.getDeclaredFields(); // all access levels, this class onlygetField/getFields sehen nur public-Felder, folgen aber der Vererbungskette. getDeclaredField/getDeclaredFields sehen auch private/protected/Paket-Felder, aber nur jene, die direkt auf der abgefragten Klasse deklariert sind. Um alle Felder inklusive vererbter privater zu sammeln, muss man getSuperclass() durchlaufen und zusammenführen.
Feld-Metadaten: Name, Typ, Modifikatoren, Generics
Ein Field beantwortet Fragen über sich selbst, ohne dass eine Instanz benötigt wird:
Field f = User.class.getDeclaredField("age");
f.getName(); // "age"
f.getType(); // int.class — the erased type
f.getGenericType(); // int — Type, keeps generic info
f.getModifiers(); // int bitset
Modifier.isPrivate(f.getModifiers()); // true/false
Modifier.isStatic(f.getModifiers());
Modifier.isFinal(f.getModifiers());
f.getDeclaringClass(); // class …UsergetType() liefert den gelöschten Class-Typ (List); getGenericType() gibt einen Type zurück, der bei einem List<String>-Feld zu ParameterizedType gecastet werden kann, um String wiederzugewinnen. Diese Wiederherstellung funktioniert, weil generische Feld-Signaturen in der Klassendatei erhalten bleiben, auch wenn Instanzen gelöscht sind.
Werte lesen und schreiben
Um zu lesen oder zu schreiben, wird eine Instanz benötigt (oder null für ein static-Feld), und die Zugriffsprüfung muss überwunden werden:
User u = new User("ada", 36);
Field age = User.class.getDeclaredField("age");
age.setAccessible(true); // bypass the access check for private
int current = age.getInt(u); // typed getter for primitives → 36
age.setInt(u, 37); // typed setter
Object boxed = age.get(u); // generic getter, autoboxes → Integer 37
age.set(u, 40); // generic setter, autounboxesFür primitive Felder gibt es typisierte Zugriffsmethoden — getInt, getBoolean, getDouble, setLong, … — sowie die generischen get(Object)/set(Object,Object) für beliebige Felder (mit Boxing von Primitiven). Für ein static-Feld wird null als Ziel übergeben: staticField.get(null).
Das setAccessible-Gate
Standardmäßig erzwingt ein Field Javas Zugriffsregeln: Das reflektive Lesen eines private-Felds wirft eine IllegalAccessException. field.setAccessible(true) unterdrückt diese Prüfung für dieses Field-Objekt. Das ist es, was Reflection in die Lage versetzt, auf interne Strukturen zuzugreifen — und was sie gefährlich macht.
Zwei Einschränkungen seit Java 9:
- Modulgrenzen. Wenn der Zieltyp in einem Modul liegt, das sein Paket nicht für einen
opens, wirftsetAccessible(true)eineInaccessibleObjectException. Bibliotheken bitten darum,--add-openshinzuzufügen oder das Paket peropensfreizugeben. - Es gilt pro Objekt. Ein Aufruf von
setAccessible(true)betrifft nur dieField-Instanz, auf der es aufgerufen wurde, nicht das Feld global. Ein neu abgerufenesFieldfür dasselbe Member startet wieder gesperrt.
final-Felder schreiben
final-Felder sind ein besonderer, heikler Fall. Bei einem nicht-statischen final-Feld kann man ihn manchmal nach setAccessible(true) noch schreiben:
Field f = Config.class.getDeclaredField("name"); // private final String
f.setAccessible(true);
f.set(config, "changed"); // may work…Aber es gibt erhebliche Einschränkungen:
- Es funktioniert nicht für
static final-Primitiv- oderString-Konstanten — diese werden vom Compiler an jeder Verwendungsstelle inline eingefügt, sodass bereits kompilierte Lesezugriffe den geänderten Wert nicht widerspiegeln, selbst wenn man das Feld ändert. - Die JVM und der JIT gehen davon aus, dass
final-Felder sich nie ändern; das Mutieren eines solchen ist undefiniertes Verhalten für die Sichtbarkeit und kann wegoptimiert werden. - Moderne JDKs verbieten es zunehmend vollständig.
Die ehrliche Regel: final-Felder reflektiv in der Produktion nicht mutieren. Serialisierungs-Frameworks, die dies tun (um unveränderliche Objekte zu rekonstruieren), verwenden Low-Level-Unsafe/VarHandle-Mechanismen und akzeptieren das Risiko bewusst. Das folgende Beispiel zeigt den instanz-finalen Fall als funktionierend, um den Mechanismus zu veranschaulichen — nicht als Empfehlung.
Ein ausgearbeitetes Beispiel: ein einfacher feldbasierter Mapper
Das Programm reflektiert über die deklarierten Felder eines Objekts, um eine Map<String,Object> zu bauen (ein Mini-Serialisierer), nimmt dann eine Map und schreibt deren Werte zurück in eine neue Instanz (ein Mini-Deserialisierer) — dabei werden durchgehend private-Felder berührt, ohne dass irgendwo Getter oder Setter vorhanden sind.
Was aus dem Lauf zu entnehmen ist:
toMaperzeugte einen Snapshot jedes Instanzfelds ohne einen einzigen Getter —getDeclaredFields()plussetAccessible(true)erreichten denprivate-Zustand direkt. Das ist im Wesentlichen das, was Jackson und Gson tun, wenn sie für Feldzugriff konfiguriert sind. Die Klasse benötigt keine spezielle API; Reflection liefert die generische.- Das statische
count-Feld wurde ausgeschlossen, weil die SchleifeModifier.isStaticprüfte. Serialisierer überspringen routinemäßigstatic-,transient- und synthetische Felder; das Modifier-Bitset ermöglicht diese Entscheidungen einheitlich, ohne Feldnamen hart zu kodieren. fromMapschrieb dasprivate final currency-Feld nachsetAccessible(true), und es trat in Kraft — was den instanz-finalen Mechanismus demonstriert. Dies funktionierte nur, weilcurrencyein nicht-statisches final ist, das vor jeder Optimierungsannahme über seine Konstanz neu zugewiesen wurde; sich in echtem Code darauf zu verlassen ist fragil, undstatic final-Konstanten hätten sich nicht geändert.- Das Lesen von Metadaten (
bal.getType(),Modifier.toString(...),isFinal(...)) benötigte überhaupt keineAccount-Instanz — einFieldbeschreibt die Deklaration, die für jedes Objekt der Klasse gleich ist. Werte benötigen eine Instanz; die Struktur nicht. - Das typisierte
getInt(rebuilt)gab den Primitivwert direkt ohne Boxing zurück, und dasstatic-Feld wurde mitcnt.get(null)gelesen —nullals Ziel zu übergeben ist die Konvention für statische Felder. Die Verwendung des typisierten Accessors für Primitive vermeidet eine Allokation pro Lesevorgang, was bei intensiven Serialisierungspfaden relevant ist.