AlgoMaster Logo

Accessing Fields & Methods

Medium Priority19 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

Reflection's two most common jobs are reading or writing a field whose name you only know at runtime, and calling a method whose signature you only resolve at runtime. This lesson covers the two APIs that do that work: Field.get/set and Method.invoke. The previous lesson covered how to discover the Field and Method objects in the first place. Now we use them.

The pattern is the same for both. You look up the Field or Method, optionally call setAccessible(true) to bypass access checks, then read or write or invoke. The interesting part is the corner cases: static members, final fields, primitives, exception unwrapping, and the cost of doing this on a hot path.

Reading a Field's Value

Once you have a Field object, you read its value with field.get(instance). The instance is the object whose field you want to read. The return type is Object, so primitives come back as their boxed equivalents and you cast or unbox at the call site.

Reading the double field came back as a boxed Double, which auto-unboxed into the double variable. That works but it allocates. For a hot loop that reads millions of values, the boxing per call adds up.

The Field class also offers typed accessors that skip the box entirely: getInt, getLong, getDouble, getBoolean, getByte, getShort, getFloat, getChar. Each one matches a primitive type. They throw IllegalArgumentException if the field is not of the matching type.

The typed accessors avoid the autobox-then-unbox round trip you get with get. The cost difference is small for one call, but if you are walking every field on every object in a large list, choose the typed variant for primitives.

field.get(obj) boxes primitives on every call. For primitive fields read in a tight loop, prefer field.getInt(obj) (or the matching primitive variant) to skip the allocation.

Writing a Field's Value

Field.set(instance, value) writes a new value into the field. The value argument is Object, so primitives are boxed at the call site, which mirrors the read API.

Just like reading, there are primitive-typed setters: setInt, setLong, setDouble, setBoolean, setByte, setShort, setFloat, setChar. They take the unboxed primitive directly.

The typed setters check the field's declared type. If you call setInt on a String field, you get IllegalArgumentException. If you call set with a value whose type does not match (passing a String to a double field, for example), you also get IllegalArgumentException. The error is at the boundary, not silent.

The exact message text varies by JDK version, but the exception type is stable. If you are writing a generic field setter, catch IllegalArgumentException and report which field and which value failed; otherwise the message alone is not always obvious to the caller.

Static Fields: Pass null as the Target

Static fields belong to the class, not to any instance. The reflection API still requires you to call get or set, and the convention is to pass null as the target object. Reflection sees the field is static, ignores the target, and reads or writes the class-level slot.

The null is not magic, it is a convention that says "no receiver because the field is static." If you pass a real instance for a static field, reflection accepts it and uses the static slot anyway. The reverse is fatal: if you pass null for a non-static field, you get NullPointerException.

Final Fields and the Module System

Final fields are designed to be assigned once and never changed. Reflection has historically been able to side-step that by calling setAccessible(true) and then set, but the rules have tightened over the years and depend on whether the field is instance-final or static-final, and on which Java version you are on.

For a non-static final field, you can usually still write to it through reflection if you call setAccessible(true) first. The JVM treats the write as "reflective override" and lets it through, though some JIT optimizations may have already inlined the original value into compiled code, so the change may not be visible everywhere you expect.

Static final fields are a different story. From Java 9 onward, the JVM blocks reflective writes to static final fields, especially ones the compiler may have folded into constants. Older tricks that modified the modifiers flag through reflection no longer work because the modifiers field on Field itself is now inaccessible to reflection on recent versions.

The exception type varies by JDK and the exact configuration of the field, but the takeaway is the same: do not try to modify a static final through reflection. The runtime considers it a real constant.

The modern alternative for legitimately mutable shared state with finer control is VarHandle, introduced in Java 9. A VarHandle lets you read and write a field with explicit memory ordering and without the reflective overhead. For most application code you will not need it, but it is the recommended path when you need atomic or volatile semantics on a field you discovered dynamically. We won't go deeper here; MethodHandle and VarHandle are a separate topic.

setAccessible(true) itself does access checks the first time it is called on a member, then caches the result on the Field or Method. Call it once during setup, reuse the object forever. Calling setAccessible repeatedly inside a loop is wasted work.

Invoking a Method

Method.invoke(target, args...) calls a method reflectively. The target is the instance to call it on, and the rest is the argument list, varargs-style. The return type is Object; primitives come back boxed; void methods return null.

The full lifecycle of a reflective method call, end to end: every reflective invocation walks the same path: locate the Method, open it up if needed, call it, then unwrap any exception it threw.

The rest of this section walks through each step. Lookup and setAccessible you have seen already; the new pieces are calling invoke and unwrapping InvocationTargetException cleanly.

The two arguments to addItem are passed positionally. Because invoke is varargs, you list them in order after the target. Primitive parameters are accepted as boxed values, and reflection unboxes them before the call. A wrong type, a wrong number of arguments, or a null for a primitive parameter raises IllegalArgumentException.

For a void method, the return value is null. There is no separate API for void; you just ignore the result or cast it where you need to.

The result is always null for a void method. If you wrote a wrapper that returned different types based on what it found, you would check the return type of the Method first and branch.

Invoking Static Methods

Static methods follow the same null convention as static fields. The target argument is ignored when the method is static, but you should pass null to be explicit about it.

The cast (double) applyDiscount.invoke(...) works because reflection returned a boxed Double, which auto-unboxes. If the method returned int you would cast to Integer and let it unbox, or cast directly to int since Java's casts handle the unbox.

setAccessible(true) and Private Members

By default, reflection respects Java's normal access rules. You can read a public field on any class. You cannot read a private field on a class you do not have access to. The setAccessible(true) call asks the runtime to skip those access checks for this Field or Method object.

The first get call hits the private access modifier and fails with IllegalAccessException. After setAccessible(true), the same get call works. The same applies to Method.invoke on a private method.

setAccessible does two things. It bypasses the language-level access modifier check (private, package-private, protected). It also bypasses the module-system access check, as long as the module that owns the type has opened the relevant package to your code. If a module has not opened the package, you cannot bypass it from outside, no matter what.

The module system arrived in Java 9 and has caused the most friction here. If you try to read a private field on a JDK internal class (something inside java.base, for example), you typically get InaccessibleObjectException, a subclass of RuntimeException:

The fix is not to find a clever workaround; it is to launch with the JVM flag --add-opens java.base/java.util=ALL-UNNAMED, which opens the package for deep reflection. Libraries like Spring and Hibernate often document the exact flags they need.

The old escape hatch, SecurityManager, is deprecated in Java 17 and removed in Java 24. It used to be the way to lock down reflection in untrusted code. Modern Java leans on the module system and explicit --add-opens instead.

setAccessible(true) does access checks once per Field/Method instance, then caches "yes." Cache the Field/Method itself (not just the lookup) and you pay this cost once for the life of the JVM.

Exception Unwrapping with InvocationTargetException

When the method you invoke throws an exception, reflection does not let that exception bubble out directly. It wraps it in InvocationTargetException and throws that instead. To get the real exception, you call getCause() on the wrapper.

The wrapper exists for a reason. Method.invoke declares throws InvocationTargetException, IllegalAccessException, IllegalArgumentException. The first one comes from the target method itself; the latter two come from the reflection machinery. Wrapping the target's exception keeps the two channels separate, so you can tell whether the call failed because reflection refused it or because the method itself blew up.

A clean pattern when you want to rethrow the underlying exception:

The helper rethrows whatever the method actually threw, so the caller catches it the same way they would in a non-reflective call. The (Object) null cast on the argument exists because invoke takes a varargs Object[]; without the cast Java would interpret a bare null as the array itself rather than a single null argument.

Three exception types you handle when invoking methods or reading fields:

ExceptionWhen it happensTypical fix
IllegalAccessExceptionThe member is not visible to your code and setAccessible(true) was not called or was blocked by the module systemCall setAccessible(true), or open the module with --add-opens
IllegalArgumentExceptionWrong argument type, wrong number of args, wrong field type for a typed accessor, or null target for a non-static memberVerify the Method/Field you looked up matches what you are passing
InvocationTargetExceptionThe method ran but threw its own exceptionCatch it, call getCause(), handle the real exception

A Small Copy-Properties Utility

Here is a practical use of everything so far. We will write a utility that copies same-named fields from a source object to a destination object. It is the kind of helper many small frameworks ship. This version handles only fields with matching names and assignable types, which keeps the code short while showing the API in context.

This walks each declared field on the source, looks for a same-named field on the destination, and copies the value when the types are compatible. isAssignableFrom checks that the destination's declared type accepts the source's declared type. Real utility libraries (Apache Commons BeanUtils, Spring's BeanUtils) layer in more rules: type coercion, ignore lists, getter/setter access instead of field access. The skeleton, though, is exactly this.

Caching Field and Method Objects

Reflection has two cost centers: looking up the Field or Method object, and then calling get/set/invoke. The lookup is by far the more expensive of the two. It walks the class's member table, allocates a new object, and on a JIT-compiled invocation may also do security and module checks.

A naive call site does the lookup every time:

Cache the Field once and reuse it:

Same result, much less work per call. The lookup happens once in the static initializer; the loop only pays the invocation cost. The same pattern applies to Method: resolve it once, store it, reuse it.

getDeclaredField and getDeclaredMethod are the slow path. Reflective get/set/invoke on a cached, accessible member is much cheaper. If a reflective call sits on a hot path, the cache is what makes it acceptable.

For frameworks that resolve fields and methods once at startup, the runtime cost is dominated by the per-call invoke. That is still slower than a direct call, but on modern JVMs the gap has narrowed because the JIT inlines reflective calls aggressively after enough invocations.

MethodHandle and VarHandle: A Brief Note

When reflective invoke ends up on your hottest code path and the JIT inlining isn't enough, the JDK offers two faster alternatives. MethodHandle (Java 7+) and VarHandle (Java 9+) live in java.lang.invoke and give you typed, JIT-friendly handles to methods and fields. The lookup is more verbose, but the runtime call is close to a direct method call.

You will not use this on a typical day. For application code, Method.invoke with a cached Method is fine. Use MethodHandle only when you can show the reflective call is the bottleneck and you have measured the difference. VarHandle plays the same role for fields, with extra support for atomic and volatile semantics. The JDK has a faster lane if reflection's overhead is a real problem.

Quiz

Accessing Fields & Methods Quiz

10 quizzes