Java

Reflection in Java: Inspecting and Modifying Classes at Runtime

Executive Summary

Reflection in Java starts with the fact that every loaded type has one Class object. You obtain it via a class literal (Book.class), an instance (book.getClass()), or Class.forName with a name. From Class you enumerate members. For example, getDeclaredMethods and getDeclaredFields give what this class declares, while getMethods and getFields add the inherited public ones. Also, isRecord and isInterface read the type’s shape.

Execution goes through the reflective classes. Constructor.newInstance builds instances, which is the supported workaround for erasure’s no-new-T rule. Meanwhile, Method.invoke calls methods with boxing overhead. Finally, Field get and set read and write state, with setAccessible(true) crossing private boundaries until the module system, the next article, restricts it. Failures are reflective: InvocationTargetException wraps the real exception thrown inside the target method, so unwrap getCause at the boundary. However, the costs are real: slower calls than direct invocation, and string-based lookups that refactoring does not track. As a result, production code caches Method and Field objects and keeps reflection to the infrastructure layer where frameworks already paid those costs for you.

The Class Object: Every Type’s Mirror

Reflection begins with Class, the runtime handle to a loaded type, documented in the Class specification and the reflection trail. In practice, three doors lead to the same object:

Class<String> byLiteral = String.class;                   // the class literal
Class<?> byInstance    = "hello".getClass();              // from any object
Class<?> byName        = Class.forName("java.time.LocalDate");  // by name: loads it

System.out.println(byLiteral.getSimpleName());      // String
System.out.println(byName.isRecord());             // false
System.out.println(Book.class.isRecord());         // true: Part 2's records are visible here
System.out.println(Runnable.class.isInterface());  // true

byName is the reflective one, and its power is the point. The type name can come from configuration, an annotation value, or a network message, so the class loads on demand. However, its cost is also the point: a typo throws ClassNotFoundException at runtime, the fragility that direct code never has.

The generics article’s erasure rule reappears here as a feature. Book.class and Sale.class are distinct Class objects, but List<String>.class does not exist, since there is one erased Class per generic type. That is exactly why reflection cannot tell you a List’s element type and why Class<T> workarounds pass the type token explicitly.

Inspecting Members

The members lesson distinguishes two families of accessors, and the distinction trips everyone once. getDeclared gives what this class declares, get gives the public surface including inherited members:

Call Returns Inherited members Private members
getMethods() Public methods Yes No
getDeclaredMethods() Everything this class declares No Yes
getFields() Public fields Yes No
getDeclaredFields() Everything this class declares No Yes
getConstructors() Public constructors – No
for (var method : Sale.class.getDeclaredMethods()) {
    System.out.println(Modifier.toString(method.getModifiers())   // "public"
            + " " + method.getReturnType().getSimpleName()        // "long"
            + " " + method.getName());                            // "totalCents"
}

Every Member knows its modifiers, its types, and its annotations, so a class dump is a few lines of composition. The previous article’s RetryRunner already used the pattern: getDeclaredMethods, filter by getAnnotation, act on the survivors. Reduced to its skeleton, that is how JUnit finds your tests.

Executing Reflectively: Construct, Invoke, Access

Next, inspection becomes execution through three operations. First, newInstance through a Constructor solves the generics article’s no-new-T problem. It is the supported way to create T given Class<T>:

Constructor<Sale> ctor = Sale.class.getConstructor(
        java.time.LocalDate.class, String.class, String.class, int.class, long.class);

Sale sale = ctor.newInstance(java.time.LocalDate.parse("2026-01-05"),
        "north", "books", 3, 899);     // constructed: no compile-time Sale import needed

Then Method.invoke calls, and Field get and set read and write, crossing the private boundary with setAccessible:

class Greeter {
    private String greeting = "hello";
    private String greet() { return greeting + " world"; }
}

var instance = new Greeter();
Class<?> c = Greeter.class;

Method greet = c.getDeclaredMethod("greet");     // by name, reflective
greet.setAccessible(true);                       // cross the private boundary
System.out.println(greet.invoke(instance));     // hello world

Field greeting = c.getDeclaredField("greeting");
greeting.setAccessible(true);
greeting.set(instance, "goodbye");              // modify private state
System.out.println(greet.invoke(instance));     // goodbye world

That four-line sequence is the honest demonstration of both the power and the warning. Reflection reads and writes what the encapsulation article locked away. That is legitimate for frameworks at their boundaries and illegitimate as a routine trick to bypass your own design.

InvocationTargetException: The Wrapped Truth

When a reflective call throws, the original exception never reaches you directly: Method.invoke wraps it. For example, here is the wrong code first, the mistake in every first reflective program:

// WRONG: the wrapper hides the real failure
try {
    method.invoke(target);
} catch (Exception e) {
    log(e.getMessage());   // null or worse: the wrapper, not the cause
}

// RIGHT: unwrap the cause at the boundary
try {
    method.invoke(target);
} catch (InvocationTargetException e) {
    Throwable real = e.getCause();      // the exception the method actually threw
    throw new IllegalStateException("invoke failed: " + method.getName(), real);
} catch (IllegalAccessException e) {
    throw new IllegalStateException("not accessible: " + method.getName(), e);
}

The delta: InvocationTargetException is an envelope, so its getCause carries the true stack trace of the method that failed. The exceptions article’s boundary-wrapping pattern applies verbatim: unwrap, add context, chain.

setAccessible and the Module Boundary

setAccessible(true) is reflective code asking the runtime to suspend an access check. In the pre-module world, that request worked for almost any private member in any classpath code. However, the module system, the next article, changed the contract. Strong encapsulation means a module that does not open a package to you can refuse. Then setAccessible throws InaccessibleObjectException. As a result, the practical consequences are two. On the classpath, the old permissiveness persists for compatibility, which is why most reflective hacks still run. On the module path, by contrast, frameworks need the module to open its packages through the module-info declarations the next article covers. When they do not, the error is explicit rather than silent.

The professional reading of that evolution is simple. The platform is closing the door reflection used to hold open, so reflective access to private internals is a shrinking asset. Frameworks that need it ask for opens explicitly, while application code that reaches into other people’s privates is living on borrowed time.

The Two Costs: Performance and Fragility

In short, reflection charges twice, and honest engineering prices both. The performance cost has three components. First, reflective calls pass arguments through an Object array with boxing. Second, every call re-checks accessibility. Third, string-based member names defeat the JIT’s best inlining assumptions. Modern JVMs narrow the gap considerably, and higher-performance alternatives like MethodHandle exist for the rare hot path. However, the order of magnitude holds: a reflective call is slower than a direct one, and the difference only matters in loops. For example, here is the wrong code first, the classic hot-loop mistake:

// WRONG: lookup inside the loop, every iteration pays
long total = 0;
for (var sale : sales) {
    Method m = sale.getClass().getMethod("totalCents");   // lookup, again and again
    total += (long) m.invoke(sale);
}

// RIGHT: resolve once, cache the Method, invoke many
private static final Method TOTAL_CENTS;
static {
    try {
        TOTAL_CENTS = Sale.class.getMethod("totalCents");
    } catch (NoSuchMethodException e) {
        throw new ExceptionInInitializerError(e);
    }
}

long total = 0;
for (var sale : sales) {
    total += (long) TOTAL_CENTS.invoke(sale);
}

The delta is the pattern every framework uses. Annotation scanning, method lookup, and field resolution happen once at startup. Then the resolved objects are cached in static or instance fields, and request-time work uses the cache. Fragility is the second cost. For example, getDeclaredMethod(“totalCents”) breaks when the method is renamed, and no compiler on earth warns you; the failure is NoSuchMethodException at runtime. That is why reflection lives at boundaries where names are contracts, like annotation-declared behavior. It never belongs inside ordinary logic where an interface call would do the same job with full compile-time checking.

Aspect Direct code Reflective code
Compile-time checking Full: signatures verified None: names resolved at runtime
Call cost Direct dispatch Boxing, accessibility checks, slower inlining
Refactoring safety IDE renames everything Strings break silently
Flexibility Types fixed at compile time Types resolved at runtime

How Real Systems Do This

In practice, every framework in this course’s future consumes reflection in Java. Jackson reflects over records’ components to map JSON fields, Part 9 shows it. Similarly, Hibernate reflects over entity classes to map fields to columns, Part 8 shows it. Likewise, JUnit reflects over test classes to find @Test methods, Part 7 shows it. However, none of them does reflection per request. Each performs the expensive scanning once, caches the resolved members, and serves requests from the cache. That is the same wrong-and-right pattern above at framework scale.

The production story that calibrated my respect for these costs was a JSON deserialization path. It reflected over an incoming payload’s target class on every call, uncached, in the hottest endpoint of a checkout service. The profiler, Part 6’s subject, showed 30 percent of the time in reflection machinery. Yet the fix was not exotic: cache the Constructor and the accessor methods, exactly as the frameworks do. In my experience, when hand-rolled reflection shows up in a profile, the finding is almost always a missing cache rather than a need for a cleverer trick. The permanent fix is usually the same, too: let a framework own the reflection, because caching it is the framework’s full-time job.

Decision Framework

  1. Does the compiler genuinely not know the type at compile time? Types from configuration, annotations, or wire formats qualify: reflection, at the boundary. Types you can name in code: an interface and a direct call.
  2. Is a framework already doing the reflection you need? Deserialization, mapping, and test discovery are framework jobs; hand-rolling them inherits the costs without the caching.
  3. Must you write it yourself? Resolve members once, cache them, and smoke test that the names still exist. For example, a unit test invoking the cached member catches renames at build time.
  4. Do you need private access? Prefer a public contract, a factory, or an opened package, in that order. Also, treat setAccessible as a negotiation that future module rules may end.
  5. Is the call in a hot path? Cache the Method or Field, or move to MethodHandle territory only after the profiler proves the cache is not enough.
  6. Are you constructing T from Class<T>? Constructor.newInstance is the supported pattern the generics article promised.

When NOT to Use This

  • Do not reflect where a direct call works. Reflection in ordinary business logic is a slower, more fragile version of code that compiles. Worse, every name is a land mine for the next refactor.
  • Do not use reflection to bypass your own encapsulation. Reaching into a private field of a class you own is a design conversation you are having with yourself via the runtime. Instead, change the design instead.
  • Do not reflect into other people’s internals, including the JDK’s. After all, module strong encapsulation exists to end exactly this, and each JDK release closes more of the doors that used to be open.

Common Mistakes

  • Catching the wrapper instead of the cause: InvocationTargetException hides the real failure, and logging the envelope logs nothing; unwrap getCause.
  • Confusing getMethods with getDeclaredMethods: one shows inherited public members, the other everything this class declares. As a result, picking the wrong one finds either too little or too much.
  • Member lookup inside loops: the uncached hot path costs the lookup on every iteration, and the fix is a resolved-and-cached field.
  • String-based names with no test: a rename becomes a runtime NoSuchMethodException. However, a one-line test invoking the cached member keeps the contract build-checked.
  • Forgetting setAccessible before private access: IllegalAccessException at the worst moment. Also, the next article shows why the module path may deny the request entirely.
  • Reflecting to avoid an interface: if you control the types, add the interface and call it directly. After all, reflection is for types you cannot see at compile time, which is a smaller set than it feels.

Key Takeaways

  • Reflection is the program reading itself: Class objects, member inspection, invocation, construction, and annotation reads, all by name at runtime.
  • One Class object per loaded type, reachable via a literal, an instance, or Class.forName; erasure means no Class for generic parameterizations.
  • getDeclared gives this class’s members including private, while the plain getters give the public surface including inherited. Confusing them is the classic first bug.
  • InvocationTargetException wraps the real failure: unwrap getCause at the boundary, per the exceptions article’s pattern.
  • Resolve once and cache: frameworks scan at startup and serve from cache. By contrast, hand-rolled reflection in a hot loop is a missing cache wearing a costume.
  • setAccessible crosses private boundaries, and the module system is closing those doors: strong encapsulation is the direction the platform is committed to.
  • Reflection belongs at infrastructure boundaries where types are unknown at compile time. Where you know the types, an interface call is faster, safer, and refactable.

FAQ

What is reflection in Java?

The API for inspecting and using classes at runtime: obtaining Class objects, enumerating methods and fields, reading annotations, invoking methods, and constructing instances, all by name. Frameworks use it to bind behavior to classes they did not compile.

How do I call a method by name in Java?

Get a Method from Class, then invoke: getDeclaredMethod(“greet”) on the Class, setAccessible(true) if the method is private, then method.invoke(instance, args). Catch InvocationTargetException and unwrap getCause, which holds the real failure.

What does setAccessible do in Java?

It suspends the access check on a field, method, or constructor so reflective code can use private members. On the module path, strong encapsulation can refuse the request, throwing InaccessibleObjectException, which is why frameworks ask modules to open packages explicitly.

Why is reflection slow in Java?

Three costs: arguments pass through an Object array with boxing, accessibility is checked per call, and string-based names limit the JIT’s inlining. Caching resolved members and running reflection once at startup removes most of it, which is how every framework survives.

What is the difference between getMethods and getDeclaredMethods?

getMethods returns public methods including inherited ones; getDeclaredMethods returns everything this class declares, including private, but nothing inherited. The same split applies to fields and constructors.

Conclusion

Reflection is the bridge between metadata and behavior: the annotations article’s data, read and acted on here. You can now read a framework’s source and recognize the skeleton: scan once, cache members, invoke per event. You also know the two costs that make the skeleton non-optional.

Next, the following article covers the boundary reflection keeps knocking on: the Java module system, JPMS. It explains module-info declarations, strong encapsulation, and the migration path that keeps the classpath world running while the module path grows stricter.

Reflection buys runtime knowledge with compile-time ignorance. Spend it only where the compiler cannot see, and cache everything you buy.

Last updated on 2 September 2026.

Share this article

Leave a Reply

Your email address will not be published. Required fields are marked *