What is Reflection?
Reflection is the ability of Java code to inspect and manipulate classes, methods, fields, and constructors at runtime — even ones it has no compile-time knowledge of. Normal Java code is bound by static types: you call methods that exist on a known type, checked by the compiler. Reflection breaks that boundary: code can ask an object "what fields do you have?" and read or modify them by name, as a string.
The problem it solves: frameworks need to operate on your classes
without knowing them in advance. Spring needs to inject dependencies into
classes it has never seen. Jackson needs to serialise any object to JSON.
JUnit needs to find and run methods annotated @Test. None of
this is possible with static method calls — the framework code is compiled
before your classes even exist.
class Person {
private String name = "Alice";
}
// Normal Java: compile-time access only
Person p = new Person();
// p.name; ❌ compile error — private, not accessible
// Reflection: runtime access by name, bypassing access control
Field nameField = p.getClass().getDeclaredField("name");
nameField.setAccessible(true);
String name = (String) nameField.get(p); // "Alice" — read via reflection
Inspecting Classes
// Three ways to get a Class object
Class<?> c1 = p.getClass(); // from an instance
Class<?> c2 = Person.class; // class literal — preferred, type-checked
Class<?> c3 = Class.forName("com.app.Person"); // by name — for dynamic loading (plugins)
Class<?> clazz = Person.class;
clazz.getName(); // "com.app.Person"
clazz.getSimpleName(); // "Person"
clazz.getSuperclass(); // parent class
clazz.getInterfaces(); // implemented interfaces
clazz.isInterface(); // false
clazz.isAnnotationPresent(Entity.class); // check for an annotation
int mods = clazz.getModifiers();
Modifier.isPublic(mods);
Modifier.isAbstract(mods);
Fields, Methods, and Constructors
Fields
Class<?> clazz = Person.class;
Person p = new Person();
clazz.getDeclaredFields(); // ALL fields of THIS class (any access level)
clazz.getFields(); // only PUBLIC fields (this class + inherited)
Field ageField = clazz.getDeclaredField("age");
ageField.setAccessible(true); // required to read/write private fields
int age = (int) ageField.get(p);
ageField.set(p, 30); // mutate even a private/final field (with caveats)
Methods
// getMethod: must specify exact parameter types — finds public methods (incl. inherited)
Method add = clazz.getMethod("add", int.class, int.class);
Object result = add.invoke(calc, 5, 3); // returns Object — caller must cast
// getDeclaredMethod: finds ANY method declared on this class (incl. private)
Method privateMethod = clazz.getDeclaredMethod("multiply", int.class, int.class);
privateMethod.setAccessible(true);
result = privateMethod.invoke(calc, 5, 3);
// Exceptions are wrapped — always unwrap the real cause
try {
add.invoke(calc, "not", "ints");
} catch (InvocationTargetException e) {
Throwable realCause = e.getCause(); // the ACTUAL exception thrown inside
}
Constructors and dynamic instantiation
// No-arg constructor
Object p2 = clazz.getDeclaredConstructor().newInstance();
// With parameters
Constructor<?> ctor = clazz.getConstructor(String.class, int.class);
Object p3 = ctor.newInstance("Alice", 25);
// Fully dynamic: load a class by name and instantiate it — the foundation
// of plugin architectures and dependency injection containers
Class<?> dynamicClass = Class.forName(pluginClassName);
Object plugin = dynamicClass.getDeclaredConstructor().newInstance();
How Frameworks Use Reflection
Almost every major Java framework you use is built on reflection. Knowing this demystifies "magic" annotations and helps you debug framework behaviour.
// Spring: @Autowired fields are populated via reflection
@Service
public class UserController {
@Autowired
private UserService userService;
}
// At startup, Spring: scans for @Autowired fields via reflection,
// resolves the bean, calls field.setAccessible(true), then field.set(controller, bean)
// Jackson: serialises by reading every field via reflection
String json = objectMapper.writeValueAsString(person);
// Internally: getDeclaredFields(), setAccessible(true) on each, field.get(person),
// build a JSON tree from the (name, value) pairs
// JUnit: finds and invokes @Test methods
public class MyTest {
@Test void shouldWork() { ... }
}
// JUnit: clazz.getDeclaredMethods(), filters by isAnnotationPresent(Test.class),
// creates a test instance via no-arg constructor, invokes each test method
// Hibernate/JPA: maps entity fields to database columns
@Entity
public class User {
@Id private Long id;
@Column private String name;
}
// Hibernate: reads @Entity/@Id/@Column via reflection at startup, builds
// a mapping, then uses reflection to populate fields from ResultSet rows
Performance: Reflection Is Slow — Cache It
// Reflection invocation: typically 10-30x slower than a direct call.
// Cost comes from: access checks on every call, argument boxing, no JIT inlining.
// ❌ WORST: look up the Method every single call
for (int i = 0; i < 1_000; i++) {
Method m = clazz.getMethod("add", int.class, int.class); // expensive lookup
m.invoke(calc, 5, 3);
}
// ✅ BETTER: cache the Method/Field/Constructor lookup once
private static final Method ADD_METHOD;
static {
try { ADD_METHOD = Calculator.class.getMethod("add", int.class, int.class); }
catch (NoSuchMethodException e) { throw new ExceptionInInitializerError(e); }
}
for (int i = 0; i < 1_000; i++) {
ADD_METHOD.invoke(calc, 5, 3); // no lookup cost, still has invocation overhead
}
// This is exactly why Spring, Hibernate, and Jackson cache reflective
// metadata aggressively at startup instead of looking it up per request.
Senior Topics: Modules and MethodHandles
The Java 9+ module problem with setAccessible()
// Before Java 9: setAccessible(true) could bypass ANY access modifier, always.
// Since Java 9 (strong encapsulation, fully enforced by default since 16/17):
// a MODULE must explicitly "open" a package before reflection can break into it.
// ❌ Without an opens declaration, this throws InaccessibleObjectException:
field.setAccessible(true);
// java.lang.reflect.InaccessibleObjectException: Unable to make field
// private java.lang.String com.app.Secret.value accessible:
// module com.app does not "opens com.app" to module spring.core
// ✅ Fix 1: declare opens in module-info.java
module com.app {
opens com.app to spring.core, com.fasterxml.jackson.databind;
}
// ✅ Fix 2: JVM flag (workaround, common during Java 9-17 migrations)
// java --add-opens com.app/com.app=ALL-UNNAMED MyApp
// This is precisely why Spring Boot, Hibernate, and Jackson required
// migration work when projects moved past Java 8 — they all rely
// heavily on setAccessible() for proxies, field injection, and serialisation.
MethodHandles — the modern, faster alternative
// java.lang.invoke.MethodHandle (Java 7+) offers near-direct-call performance
// after JIT warms up — the JVM can inline through a MethodHandle, but NOT
// through reflective Method.invoke().
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = MethodType.methodType(int.class, int.class, int.class);
MethodHandle addHandle = lookup.findVirtual(Calculator.class, "add", type);
int result = (int) addHandle.invoke(calc, 5, 3); // type-checked at the invoke() call
/*
* Reflection vs MethodHandle:
*
* Method.invoke() MethodHandle.invoke()
* ────────────────── ──────────────────────
* Access check every call Access check once, at lookup
* Args boxed in Object[] Args passed natively (with invokeExact)
* JIT cannot inline easily JIT CAN inline after warm-up
* Simpler, more common API More verbose, more performant
*
* Used internally by: java.lang.invoke (lambdas use it!), modern
* serialisation libraries, and high-performance frameworks.
*/
// Lambdas themselves are implemented via invokedynamic + MethodHandles —
// NOT via reflection. This is why method references (String::toUpperCase)
// have essentially zero overhead compared to a direct call.
Security: setAccessible() is a real attack surface
class Secrets {
private static final String API_KEY = "sk-secret-123";
}
// Reflection bypasses ALL access control — private, final, everything
Field f = Secrets.class.getDeclaredField("API_KEY");
f.setAccessible(true);
String stolen = (String) f.get(null);
// Mitigation: the Java 9+ module system's "opens" requirement is the
// CURRENT mechanism for restricting this — only modules you explicitly
// open are reflectively accessible from outside. (SecurityManager,
// the older mechanism, is deprecated for removal since Java 17 — do
// not rely on it for new code.)
When to Use Reflection
| ✅ Good fit | ❌ Avoid |
|---|---|
| Building frameworks/libraries | Regular application business logic |
| Dependency injection containers | Anywhere interfaces/polymorphism would work |
| Serialisation (JSON, XML mapping) | Performance-critical hot paths |
| Testing frameworks, mocking | Simple getter/setter access |
| Plugin architectures (dynamic loading) | Anything an IDE should be able to refactor safely |
Reflection breaks compile-time type safety — errors that would be caught
by the compiler instead surface as NoSuchMethodException or
ClassCastException at runtime. It also breaks IDE refactoring:
renaming a field doesn't update "fieldName" string literals
used in reflective lookups. Use reflection only when there is genuinely no
static alternative — which, for application code, is almost never.
Interview Questions
Q: What is reflection and what problem does it solve?
Reflection lets code inspect and invoke classes, methods, and fields at
runtime, even ones unknown at compile time. It solves the problem of
frameworks needing to operate generically on application code they've never
seen — Spring injecting into your classes, JUnit finding your test methods,
Jackson serialising your objects to JSON.
Q: What is the difference between getMethod() and getDeclaredMethod()?
getMethod() returns only public methods, including
inherited ones from superclasses and interfaces. getDeclaredMethod()
returns any method declared directly on that class — public, private,
protected, or package-private — but does NOT include inherited methods.
Q: Why is reflection slower than a direct method call?
Every reflective call performs an access check, boxes primitive arguments
into Object[], and cannot be inlined by the JIT compiler the way
a direct call can. Typical overhead is 10-30x compared to direct invocation.
Mitigate by caching the Method/Field lookup once
rather than repeating getMethod() on every call.
Q: How did the Java 9 module system change reflection?
Before Java 9, setAccessible(true) could bypass any access
modifier unconditionally. Java 9 introduced strong encapsulation: a module
must explicitly declare opens package to otherModule in its
module-info.java before reflective access from outside is
permitted — otherwise it throws InaccessibleObjectException.
This is enforced by default since Java 16/17. It broke many frameworks
(Spring, Hibernate, Mockito) during migration because they rely on deep
reflection for proxies and field injection — fixed with explicit
opens declarations or --add-opens JVM flags as a
bridge.
Q: What is the difference between Method.invoke() and MethodHandle, and why does it matter?
Both invoke methods reflectively, but MethodHandle
(java.lang.invoke, Java 7+) performs its access check once at
lookup time rather than on every call, passes arguments without boxing
(via invokeExact), and — critically — the JIT compiler CAN
inline through a MethodHandle after warm-up, while it essentially cannot
through Method.invoke(). This is why lambdas and method
references compile to invokedynamic + MethodHandles instead of
classic reflection: near-zero overhead at steady state.
Q: Why is SecurityManager not the answer to reflection security in 2026?
SecurityManager has been deprecated for removal since Java 17
(JEP 411) due to performance overhead and a complexity that almost nobody
used correctly. The current and forward-looking mechanism for restricting
reflective access is the module system's opens directive —
explicit, declarative, and checked by the module system itself rather than
a pluggable runtime policy. New code should not architect security around
SecurityManager.