W3docs

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 only

getField/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 …User

getType() 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, autounboxes

Fü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, wirft setAccessible(true) eine InaccessibleObjectException. Bibliotheken bitten darum, --add-opens hinzuzufügen oder das Paket per opens freizugeben.
  • Es gilt pro Objekt. Ein Aufruf von setAccessible(true) betrifft nur die Field-Instanz, auf der es aufgerufen wurde, nicht das Feld global. Ein neu abgerufenes Field fü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- oder String-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.

java— editable, runs on the server

Was aus dem Lauf zu entnehmen ist:

  • toMap erzeugte einen Snapshot jedes Instanzfelds ohne einen einzigen Getter — getDeclaredFields() plus setAccessible(true) erreichten den private-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 Schleife Modifier.isStatic prüfte. Serialisierer überspringen routinemäßig static-, transient- und synthetische Felder; das Modifier-Bitset ermöglicht diese Entscheidungen einheitlich, ohne Feldnamen hart zu kodieren.
  • fromMap schrieb das private final currency-Feld nach setAccessible(true), und es trat in Kraft — was den instanz-finalen Mechanismus demonstriert. Dies funktionierte nur, weil currency ein nicht-statisches final ist, das vor jeder Optimierungsannahme über seine Konstanz neu zugewiesen wurde; sich in echtem Code darauf zu verlassen ist fragil, und static final-Konstanten hätten sich nicht geändert.
  • Das Lesen von Metadaten (bal.getType(), Modifier.toString(...), isFinal(...)) benötigte überhaupt keine Account-Instanz — ein Field beschreibt 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 das static-Feld wurde mit cnt.get(null) gelesen — null als 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.

Übungen

Übung
Eine JSON-Bibliothek deserialisiert in eine Klasse mit 'private'-Feldern und ohne Setter. Per Reflection ruft sie 'getDeclaredField(name)' und dann 'field.set(obj, value)' auf, erhält aber beim ersten privaten Feld eine 'IllegalAccessException'. Welcher einzelne Aufruf behebt das, und warum ist er nötig?
Eine JSON-Bibliothek deserialisiert in eine Klasse mit 'private'-Feldern und ohne Setter. Per Reflection ruft sie 'getDeclaredField(name)' und dann 'field.set(obj, value)' auf, erhält aber beim ersten privaten Feld eine 'IllegalAccessException'. Welcher einzelne Aufruf behebt das, und warum ist er nötig?
Was this page helpful?