Java Reflection: Inspecting Fields
Inspect, read, and modify fields at runtime in Java with the reflection API.
Java Reflection: Inspecting Fields
A Field object describes one field of a class: its name, its type, its modifiers, and — given an instance — its value. Reflection lets you list a class's fields, read them, and write them, even when they're private and have no getter or setter. This is exactly how JSON deserializers populate objects and how ORMs hydrate entities. This chapter covers obtaining Field objects, reading and writing values, the setAccessible gate, and the special case of final fields.
Getting Field objects
The same public-vs-declared split from the intro applies:
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 only ever see public fields but follow the inheritance chain. getDeclaredField/getDeclaredFields see private/protected/package fields too, but only those literally declared on the class you ask. To collect every field including inherited private ones, walk getSuperclass() and merge.
Field metadata: name, type, modifiers, generics
A Field answers questions about itself without needing any instance:
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() gives the erased Class (List); getGenericType() returns a Type that, for a List<String> field, you can downcast to ParameterizedType to recover String. That recovery works because generic field signatures are retained in the class file even though instances are erased.
Reading and writing values
To read or write, you need an instance (or null for a static field) and you must get past the access check:
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, autounboxesThere are typed accessors — getInt, getBoolean, getDouble, setLong, … — for primitive fields, and the generic get(Object)/set(Object,Object) for any field (boxing primitives). For a static field, pass null as the target: staticField.get(null).
The setAccessible gate
By default a Field enforces Java's access rules: reading a private field reflectively throws IllegalAccessException. field.setAccessible(true) suppresses that check for this Field object. It's what makes reflection able to touch internals — and what makes it dangerous.
Two caveats since Java 9:
- Module boundaries. If the target type is in a module that hasn't
opensits package to you,setAccessible(true)throwsInaccessibleObjectException. Libraries ask you to add--add-opensor the module toopensthe package. - It's per-object. Calling
setAccessible(true)affects only theFieldinstance you called it on, not the field globally. A freshly looked-upFieldfor the same member starts locked again.
Writing final fields
final fields are a special, fraught case. For a non-static final field you can sometimes still write it after setAccessible(true):
Field f = Config.class.getDeclaredField("name"); // private final String
f.setAccessible(true);
f.set(config, "changed"); // may work…But there are heavy caveats:
- It does not work for
static finalprimitive orStringconstants — those are inlined by the compiler at every use site, so even if you change the field the already-compiled reads won't reflect it. - The JVM and JIT assume
finalfields never change; mutating one is undefined behaviour for visibility and can be optimized away. - Modern JDKs increasingly forbid it outright.
The honest rule: don't mutate final fields reflectively in production. Serialization frameworks that do it (to reconstruct immutable objects) use low-level Unsafe/VarHandle machinery and accept the risk deliberately. The example below shows the instance-final case working to illustrate the mechanism, not as a recommendation.
A worked example: a tiny field-based mapper
The program reflects over an object's declared fields to build a Map<String,Object> (a mini serializer), then takes a map and writes its values back into a fresh instance (a mini deserializer) — touching private fields throughout, with no getters or setters anywhere.
What to take from the run:
toMapproduced a snapshot of every instance field without a single getter —getDeclaredFields()plussetAccessible(true)reached theprivatestate directly. That is, mechanically, what Jackson and Gson do when configured for field access. The class needs no special API; reflection supplies the generic one.- The static
countfield was excluded because the loop testedModifier.isStatic. Serializers routinely skipstatic,transient, and synthetic fields; the modifier bitset is how you make those decisions uniformly instead of hard-coding field names. fromMapwrote theprivate final currencyfield aftersetAccessible(true)and it took effect — demonstrating the instance-final mechanism. This worked only becausecurrencyis a non-static final reassigned before any optimizer assumed it constant; relying on this in real code is fragile, andstatic finalconstants would not have budged.- Reading metadata (
bal.getType(),Modifier.toString(...),isFinal(...)) needed noAccountinstance at all — aFielddescribes the declaration, which is the same for every object of the class. Values need an instance; shape does not. - The typed
getInt(rebuilt)returned the primitive directly without boxing, and thestaticfield read usedcnt.get(null)— passingnullas the target is the convention for statics. Picking the typed accessor for primitives avoids an allocation per read, which matters in hot serialization paths.
Practice
A JSON library deserializes into a class that has 'private' fields and no setters. Using reflection it calls 'getDeclaredField(name)' then 'field.set(obj, value)', but gets 'IllegalAccessException' on the first private field. Adding which single call fixes it, and why is it needed?