Back to Blog
Java

Java Reflection: Inspecting and Modifying Runtime Behavior

java reflection: Learn how to inspect classes, invoke methods, and access fields at runtime with Java reflection, including performance tradeoffs.

reflectionruntimeclassmethodfield
Illustration of Java reflection concept showing a class being inspected at runtime

When you need to inspect or modify the structure of a class at runtime, Java reflection is the mechanism that makes it possible. Reflection lets you read annotations, list methods, invoke constructors, and even access private fields without knowing the class at compile time. It powers frameworks like Spring, Hibernate, and Jackson, and it is also the source of many subtle runtime failures. This article explains how reflection works, where it is appropriate, and what costs you incur when you use it.

The Class Object and Runtime Type Information

Every Java object has a corresponding Class object that holds metadata about its type. You can obtain it in three ways: obj.getClass(), ClassName.class, or Class.forName("fully.qualified.Name"). The Class object is the entry point for all reflective operations.

// Obtaining a Class object Class<?> clazz1 = "hello".getClass(); Class<?> clazz2 = String.class; Class<?> clazz3 = Class.forName("java.lang.String");

The Class object exposes methods like getDeclaredFields(), getDeclaredMethods(), and getDeclaredConstructors(). These return arrays of Field, Method, and Constructor objects, each of which can be used to inspect or modify the corresponding member.

Inspecting Fields, Methods, and Constructors

To list all public fields of a class, use getFields(). To get all fields regardless of access modifier, use getDeclaredFields(). The same distinction applies to methods and constructors. Here is an example that prints the names of all fields and methods in a simple class:

public class User { private String name; public int age; public void setName(String name) { this.name = name; } private void secret() {} } Class<?> userClass = User.class; for (Field field : userClass.getDeclaredFields()) { System.out.println(field.getName() + " : " + field.getType().getName()); } for (Method method : userClass.getDeclaredMethods()) { System.out.println(method.getName() + " : " + method.getReturnType().getName()); }

This prints name : java.lang.String, age : int, setName : void, and secret : void. Note that getDeclaredMethods() includes private methods, but it does not include inherited methods. If you need inherited members, use getMethods() for public members only.

Invoking Methods and Accessing Private Members

Reflection allows you to call methods that are not accessible through normal compile-time checks. For example, you can invoke a private method by setting it accessible first.

Method secretMethod = User.class.getDeclaredMethod("secret"); secretMethod.setAccessible(true); secretMethod.invoke(new User());

The setAccessible(true) call is necessary for private or protected members. It suppresses Java's access control checks. This works in most environments, but it can fail under a security manager or when running on a module path with strong encapsulation (Java 9+). In those cases, you may need to add --add-opens JVM flags.

Accessing private fields follows the same pattern:

Field nameField = User.class.getDeclaredField("name"); nameField.setAccessible(true); User user = new User(); nameField.set(user, "Alice"); System.out.println(nameField.get(user)); // prints Alice

Be aware that invoke and get/set wrap checked exceptions in InvocationTargetException and IllegalAccessException. Always unwrap the cause when debugging, because the original exception is often more informative.

Performance Cost of Reflection

Reflection is slower than direct method calls for several reasons: it performs runtime type checks, resolves method signatures, and boxes primitive arguments. Each call to Method.invoke also creates an array of Object for parameters. The JIT compiler can sometimes optimize reflective calls after they have been executed many times, but the overhead is still measurable.

A common pattern is to cache Method or Field objects instead of looking them up on every invocation. The lookup itself is expensive because it scans the class metadata. Caching the Method object avoids repeated lookups, but the invoke call still has overhead compared to a direct call.

If you need high performance and are using Java 7 or later, consider java.lang.invoke.MethodHandle. Method handles provide a more efficient and type-safe way to invoke methods reflectively. They also allow the JIT to inline calls in some cases.

MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle handle = lookup.findVirtual(User.class, "setName", MethodType.methodType(void.class, String.class)); handle.invokeExact(new User(), "Bob");

Method handles are not a drop-in replacement for reflection, but they are worth using when performance matters and you control the invocation pattern.

Alternatives: MethodHandles and LambdaMetafactory

For scenarios where you need to invoke a method repeatedly, MethodHandle is often a better choice than Method. It has a more verbose API but offers better performance and compile-time type checking through MethodType. Another alternative is LambdaMetafactory, which can generate a functional interface implementation that calls a target method. This is what the :: operator compiles to under the hood.

MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle setter = lookup.findVirtual(User.class, "setName", MethodType.methodType(void.class, String.class)); UserFunction func = (UserFunction) LambdaMetafactory.metafactory( lookup, "apply", MethodType.methodType(UserFunction.class), MethodType.methodType(void.class, User.class, String.class), setter, MethodType.methodType(void.class, User.class, String.class) ).getTarget().invokeExact(); func.apply(new User(), "Charlie");

This approach has the lowest runtime overhead after warmup, but it requires generating a functional interface and is more complex to set up. For most applications, the added complexity is not justified unless you are building a framework or a hot path.

Common Pitfalls and Maintainability Concerns

Reflection breaks compile-time type safety. A typo in a field name or method signature will not surface until runtime, often as a confusing NoSuchFieldException or NoSuchMethodException. This makes code harder to refactor and debug. Use reflection only when you cannot achieve the same behavior with generics, interfaces, or a visitor pattern.

Another issue is that reflection can violate encapsulation. Setting a private field to an invalid value may put an object into an inconsistent state. If you must do this, document the invariant and keep the reflective code isolated in a well-tested utility class.

Finally, remember that reflection is not a magic wand for dynamic behavior. Many problems that seem to require reflection can be solved with a Map<String, Object> or a simple interface. The decision should be based on whether you truly need to discover and operate on unknown types at runtime. If the set of types is known at compile time, prefer a type-safe approach.

java reflection: Practical Usage and Code Examples | RYUSLOG DEV