A guide to Java reflection
Java reflection end to end: Class objects, getX versus getDeclaredX, invoking methods and reading fields, setAccessible under strong encapsulation (JEP 403, --add-opens), inflation and JEP 416 performance, MethodHandles, security and when to use it.
On this page
🪞 What is Java reflection?
Reflection is the API in java.lang.reflect (plus java.lang.Class) that lets running code look at classes, methods, fields and constructors as objects, and then call or change them by name. CardPayment.class.getDeclaredMethod("capture").invoke(payment) calls capture() without the compiler ever seeing a direct call. It's how frameworks wire objects they have never been compiled against.
For this lesson, we'll focus on the core API you'll be quizzed on: getting Class objects, the getX versus getDeclaredX split, invoking methods, reading fields, setAccessible and the module wall that went up in Java 16 and 17, and what reflection costs. Interviewers like this topic because it sits right under every framework you use. If you can explain reflection, you can explain dependency injection, ORMs and test runners.
Below the line (out of scope)
- Annotations and how to read them (next lesson: Built-in and custom annotations)
- Proxies, classpath scanning and DI containers (How frameworks use annotations and reflection)
- Bytecode generation libraries in depth,
Unsafe, and the class loader hierarchy
The 30-second answer
Every loaded class has one java.lang.Class object, and from it you get Method, Field and Constructor objects you can call or modify at runtime. getMethods() returns public members including inherited ones; getDeclaredMethods() returns every member declared in that class, any access level, nothing inherited. Private members need setAccessible(true), and since Java 16 (made permanent by JEP 403 in Java 17) that fails with InaccessibleObjectException on JDK internals unless the package is opened with --add-opens. Reflection is slower than a direct call and skips compile-time checks, so I use it for frameworks and tooling, not business logic. Since Java 18 (JEP 416) Method.invoke runs on method handles.
What you’ll learn
- Get a
Classobject three ways and know which one initialises the class - Explain
getMethods()versusgetDeclaredMethods()(public plus inherited versus everything declared here) - Invoke methods, read and write fields and call constructors, and unwrap
InvocationTargetException - Explain why
setAccessible(true)throwsInaccessibleObjectExceptionon JDK internals since Java 16/17 and how--add-opensfixes it - Describe what reflection costs: lookup copies, inflation before Java 18, and the JEP 416 method-handle rewrite
- Decide when reflection is the right tool and when
MethodHandle, generated code or plain interfaces are better
Interview radar
Asked very often: What's the difference between getMethods() and getDeclaredMethods(), and can reflection access private fields?
Senior rounds probe: Why setAccessible throws InaccessibleObjectException on Java 17, what inflation was, and how JEP 416 changed Method.invoke.
Production angle: An upgrade from Java 11 to 17 that crashes a library at startup with "module java.base does not opens java.lang".
01Picture it first: a mirror held up to every class
Think of reflection as a mirror held up to your code. When the JVM loads CardPayment, it parses the class file into internal metadata, and it also builds one Java object, the Class<CardPayment> mirror, that lets Java code see that metadata. Ask the mirror for its methods and you get Method objects: little descriptors that say "a method called capture, takes nothing, returns void, is public". Hand a Method an instance and it calls the real code.
Here's how the pieces sit in memory: your objects and the Class mirror live on the heap, the real metadata lives in Metaspace, and the reflection objects you get back are copies built from that metadata.
An order object on the heap points to its Class mirror, which points to class metadata in Metaspace and hands out copied Method, Field and Constructor objects
Notice that there is exactly one Class object per class per class loader. That's why a.getClass() == CardPayment.class is a valid identity check, and why two web apps that load the same class through different loaders see two different Class objects.
02Reflection under the hood: Class, Method, Field, Constructor
Three ways to get a Class object
Try it yourself first
Before reading on, guess which line prints "Order initialised" and whether the comparison is true.
class Order {
static { System.out.println("Order initialised"); }
}
Class<?> literal = Order.class; // nothing printed
System.out.println("after literal");
Class<?> byName = Class.forName("com.shop.Order"); // prints: Order initialised
System.out.println(literal == byName); // prints: true
System.out.println(int.class.getName()); // prints: int
System.out.println(String[].class.getName()); // prints: [Ljava.lang.String;
Notice three things:
- A class literal (
Order.class) loads the class but does not initialise it, so no static block runs.obj.getClass()needs an instance, so the class is already initialised by then. Class.forName(name)loads and initialises. That's why old JDBC code wroteClass.forName("com.mysql.jdbc.Driver"): the driver's static block registered itself. UseClass.forName(name, false, loader)when you want to inspect without running static code.- Primitives and arrays have
Classobjects too. Array names use the JVM descriptor format, which surprises people the first time they see[Ljava.lang.String;.
From a Class you can read almost everything the compiler knew: getSuperclass(), getInterfaces(), getModifiers() (decode with Modifier.toString), getPackageName(), isInterface(), isRecord() and getRecordComponents() (Java 16), getPermittedSubclasses() for sealed types (Java 17), and generic signatures through getGenericSuperclass(). Generic type arguments of a declaration survive erasure because they're stored in the class file's Signature attribute; the type argument of a particular new ArrayList<String>() object does not.
getX versus getDeclaredX
This is the most asked reflection question, and the names don't help. Here's a small hierarchy.
class Payment {
public String id() { return "p-1"; }
protected void audit() { }
}
class CardPayment extends Payment {
private String maskedPan = "4111********1111";
public long amount = 2500;
public void capture() { System.out.println("captured " + amount); }
private String tokenize(String pan) { return "tok_" + pan.substring(pan.length() - 4); }
public void refund(long cents) {
if (cents > amount) throw new IllegalArgumentException("refund > amount");
}
}
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- Real world: where reflection shows up and where it hurts
- Advanced: performance, inflation and JEP 416
- How to say it in the interview
From ₹199 · one-time payment · No auto-renewal
Further reading (9)
- java.lang.Class (Java SE 21 javadoc)
- java.lang.reflect.AccessibleObject (Java SE 21 javadoc)
- JEP 396: Strongly Encapsulate JDK Internals by Default
- JEP 403: Strongly Encapsulate JDK Internals
- JEP 416: Reimplement Core Reflection with Method Handles
- JEP 411: Deprecate the Security Manager for Removal
- The Java Tutorials: The Reflection API
- Effective Java, 3rd ed., Item 65: Prefer interfaces to reflection
- Baeldung: Guide to Java Reflection