Course contents

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.

PremiumOften askedLesson15 min readSeniorReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: a mirror held up to every class
  2. Reflection under the hood: Class, Method, Field, Constructor
  3. Real world: where reflection shows up and where it hurts
  4. Advanced: performance, inflation and JEP 416
  5. How to say it in the interview

🪞 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)

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 Class object three ways and know which one initialises the class
  • Explain getMethods() versus getDeclaredMethods() (public plus inherited versus everything declared here)
  • Invoke methods, read and write fields and call constructors, and unwrap InvocationTargetException
  • Explain why setAccessible(true) throws InaccessibleObjectException on JDK internals since Java 16/17 and how --add-opens fixes 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.

Fig. 1

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

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:

  1. 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.
  2. Class.forName(name) loads and initialises. That's why old JDBC code wrote Class.forName("com.mysql.jdbc.Driver"): the driver's static block registered itself. Use Class.forName(name, false, loader) when you want to inspect without running static code.
  3. Primitives and arrays have Class objects 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

Practise this topicReflection & annotations: interview questions & scenarios
Further reading (9)