Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 26 – Advanced language and platform features

Reflection, method handles and VarHandles

Inspect classes and records with Java reflection, call methods through method handles, update fields atomically with VarHandles, and know the limits.

  • Advanced
  • 24 minutes
  • Examples run with Java 25.0.4.1 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 9 of 24 minutes free
Free: Inspecting a record at run time · One method, three ways to call it · Reflection, method handles and VarHandles compared. With a pass: 2 in-depth parts and 3 interview answers, about 15 minutes.

What you will learn

  • Inspect classes, methods, fields and record components with reflection
  • Call methods through MethodHandles and access fields with VarHandle
  • Measure what reflection and method handles cost against a direct call
  • Explain how strong encapsulation and final fields limit deep reflection

Before you start

What you’ll learn · Before you start

What you will learn

  • Inspect classes, methods, fields and record components with reflection
  • Call methods through MethodHandles and access fields with VarHandle
  • Measure what reflection and method handles cost against a direct call
  • Explain how strong encapsulation and final fields limit deep reflection

Before you start

On this page

Java reflection lets a running program look at its own classes and use them by name: list the fields, methods, constructors and record components of a class, read a field, call a method, create an object. It lives in java.lang.reflect, and test runners, serializers and dependency-injection containers are built on it. Method handles, in java.lang.invoke, are typed references to a method, a constructor or a field that can be called directly. Their access is checked once, when the handle is looked up, instead of on every call. VarHandles are typed references to a variable, a field or an array element, with access modes that plain reflection lacks, such as compareAndSet and getAndAdd. All three trade compile-time checks for run-time flexibility, so mistakes show up only when the code runs, and since JDK 17 strong encapsulation stops them from reaching into the JDK’s own internals. Every session here was recorded on Eclipse Temurin 25.0.4.1, with comparisons on JDK 27.

Inspecting a record at run time

Csv.write turns a list of any record type into CSV without knowing the type in advance: the record’s components give the column names, and each component’s accessor method reads its value:

inspect/Csv.java Java · inspect/Csv.java
import java.lang.reflect.RecordComponent;
import java.util.List;
import java.util.StringJoiner;

/** A CSV writer for any record, written once: reflection supplies the column names and reads every value. */
record Ticket(String pnr, String from, String to, int farePaise) {}
record Reading(String sensor, double celsius) {}

public class Csv {
    static <R extends Record> String write(Class<R> type, List<R> rows) throws ReflectiveOperationException {
        RecordComponent[] components = type.getRecordComponents();
        StringJoiner out = new StringJoiner("\n", "", "\n");
        StringJoiner header = new StringJoiner(",");
        for (RecordComponent c : components) header.add(c.getName());
        out.add(header.toString());
        for (R row : rows) {
            StringJoiner line = new StringJoiner(",");
            for (RecordComponent c : components) line.add(field(c.getAccessor().invoke(row)));
            out.add(line.toString());
        }
        return out.toString();
    }

    /** RFC 4180 style: a value with a comma, a quote or a line break goes in quotes, with its quotes doubled. */
    static String field(Object value) {
        String s = String.valueOf(value);
        boolean quote = s.contains(",") || s.contains("\"") || s.contains("\n");
        return quote ? "\"" + s.replace("\"", "\"\"") + "\"" : s;
    }

    public static void main(String[] args) throws ReflectiveOperationException {
        System.out.print(write(Ticket.class, List.of(
                new Ticket("4521837690", "NDLS", "BCT", 2_150_00),
                new Ticket("8812004533", "SBC", "MAS, Chennai", 655_00))));
        System.out.print(write(Reading.class, List.of(new Reading("roof", 31.5))));
        for (RecordComponent c : Ticket.class.getRecordComponents()) {
            System.out.println(c.getName() + ": " + c.getType().getSimpleName() + ", read by " + c.getAccessor().getName() + "()");
        }
        System.out.println("String.class.getRecordComponents(): " + String.class.getRecordComponents());
    }
}
One writer, two record types Python · inspect_session.py
from session import run

# The CSV writer for any record, run on two record types, then what reflection says about one of them.
run("""
javac -d out inspect/Csv.java
java -cp out Csv
""")

Output

# JDK in use: openjdk version "25.0.4.1" LTS
$ javac -d out inspect/Csv.java
$ java -cp out Csv
pnr,from,to,farePaise
4521837690,NDLS,BCT,215000
8812004533,SBC,"MAS, Chennai",65500
sensor,celsius
roof,31.5
pnr: String, read by pnr()
from: String, read by from()
to: String, read by to()
farePaise: int, read by farePaise()
String.class.getRecordComponents(): null

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 inspect_session.py

Class.getRecordComponents() returns the components in the order the record declares them, each with its name, its type and its accessor, and it returns null for a class that is not a record, as the last line shows. The writer calls getAccessor().invoke(row) for every value, which is Java reflection at its most typical: one piece of code that works for every record, written without knowing any of them. The price is that a mistake, such as passing the wrong class, is found only at run time.

The same Class object answers many other questions. getDeclaredFields() and getDeclaredMethods() return what the class itself declares, private members included, while getFields() and getMethods() return only public members, inherited ones included. isRecord(), isSealed() and getPermittedSubclasses() describe the newer kinds of class, and Modifier decodes whether a member is static, final or private. None of these arrays comes in a guaranteed order, except the record components.

One method, three ways to call it

Calls calls Fare.withTax directly, through a Method object and through a method handle, then makes the same calls go wrong:

calls/Calls.java Java · calls/Calls.java
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;

/** One method called directly, through reflection and through a method handle, with right and wrong arguments. */
public class Calls {
    static final class Fare {
        private final int paise;

        Fare(int paise) { this.paise = paise; }

        int withTax(int percent) {
            if (percent < 0) throw new IllegalArgumentException("negative tax rate " + percent);
            return paise + paise * percent / 100;
        }
    }

    public static void main(String[] args) throws Throwable {
        Fare fare = new Fare(10_000);
        System.out.println("direct call:   " + fare.withTax(18));

        Method method = Fare.class.getDeclaredMethod("withTax", int.class);
        System.out.println("reflection:    " + method.invoke(fare, 18));

        MethodHandle handle = MethodHandles.lookup().findVirtual(Fare.class, "withTax", MethodType.methodType(int.class, int.class));
        System.out.println("method handle: " + (int) handle.invokeExact(fare, 18) + ", its type " + handle.type());

        try {
            method.invoke(fare, -1);
        } catch (Exception e) {
            System.out.println("reflection threw " + e.getClass().getSimpleName() + ", cause: " + e.getCause());
        }
        try {
            int ignored = (int) handle.invokeExact(fare, -1);
        } catch (IllegalArgumentException e) {
            System.out.println("method handle threw " + e);
        }
        try {
            method.invoke(fare, "18");
        } catch (IllegalArgumentException e) {
            System.out.println("reflection with a String argument: " + e);
        }
        try {
            long wrong = (long) handle.invokeExact(fare, 18);
        } catch (Throwable e) {
            System.out.println("invokeExact with a long result: " + e);
        }
        System.out.println("invoke adapts the types: " + (long) handle.invoke(fare, Integer.valueOf(18)));
    }
}
Direct, reflective and method-handle calls, and their failures Python · calls_session.py
from session import run

# One method, three ways to call it, and what each way does with a failing call and with wrong types.
run("""
javac -d out calls/Calls.java
java -cp out Calls
""")

Output

# JDK in use: openjdk version "25.0.4.1" LTS
$ javac -d out calls/Calls.java
$ java -cp out Calls
direct call:   11800
reflection:    11800
method handle: 11800, its type (Fare,int)int
reflection threw InvocationTargetException, cause: java.lang.IllegalArgumentException: negative tax rate -1
method handle threw java.lang.IllegalArgumentException: negative tax rate -1
reflection with a String argument: java.lang.IllegalArgumentException: argument type mismatch
invokeExact with a long result: java.lang.invoke.WrongMethodTypeException: handle's method type (Fare,int)int but found (Fare,int)long
invoke adapts the types: 11800

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 calls_session.py

All three returned 11,800. The failures differ. Method.invoke wrapped the method’s own exception in an InvocationTargetException, and it accepted a String argument until it ran, then refused it as an argument type mismatch. The method handle threw the method’s exception as it is, and invokeExact insisted on the exact type (Fare,int)int: asking for a long result threw WrongMethodTypeException, while invoke converted the boxed argument and the result for us. invokeExact is the strict and cheaper form, because no conversion runs; invoke suits code that only learns the types at run time, at the price of the conversions it adds.

A method handle starts from a Lookup. MethodHandles.lookup() carries the access rights of the class that calls it, so the handles it creates reach exactly what that class could call directly, here a method of a nested class that is not public. A MethodType describes the return type and the parameter types, and findVirtual, findStatic, findConstructor and findGetter turn a class, a name and a type into a handle, or throw at once if no such member is accessible.

Reflection, method handles and VarHandles compared

From a Class: reflection checks every call, a method handle checks once at lookup, a VarHandle adds atomic field access.Class<Fare>found by name or by .classReflection: Method withTaxmethod.invoke(fare, 18)checks access and argument typeson every call; returns ObjectMethod handle: lookup.findVirtual(…)checks access once, at the lookup;invokeExact(fare, 18) must matchthe type (Fare,int)int exactlyVarHandle: lookup.findVarHandle(…)reads and writes a field withaccess modes such as getVolatile,compareAndSet and getAndAddgetDeclaredMethodor: a Lookupfor fields: a Lookup

Three ways from a class to a call

Text description of the diagram

The diagram is a column of four boxes joined by arrows.

  1. The class, Class<Fare>, found by name or with Fare.class.
  2. An arrow labelled "getDeclaredMethod" leads to reflection: a Method object for withTax, called with method.invoke(fare, 18). Reflection checks access and the argument types on every call and returns the result as an Object.
  3. An arrow labelled "or: a Lookup" leads to a method handle, created with lookup.findVirtual. Access is checked once, when the handle is looked up, and a call with invokeExact(fare, 18) must match the handle's type, (Fare,int)int, exactly.
  4. An arrow labelled "for fields: a Lookup" leads to a VarHandle, created with lookup.findVarHandle, which reads and writes a field with access modes such as getVolatile, compareAndSet and getAndAdd.
API What is checked Use it for
Reflection, in java.lang.reflect Access and types, on every call Finding what a class has
Method handles, in java.lang.invoke Access once; the exact type per call Calling a known member often
VarHandles, in java.lang.invoke Access once; the variable’s type Atomic or ordered field updates

Java reflection is the tool for finding out what a class has; method handles are the tool for calling something you have already found, many times; VarHandles are the tool for atomic access to a field without a lock. They also combine: Lookup.unreflect turns a Method found by reflection into a method handle. Application code rarely needs any of them, and keeping them at the edges, in the framework or library that maps or injects, leaves the rest of the code checked by the compiler. When the operations are known in advance, an interface or a lambda is the better tool: a Function<Ticket, String> is a typed reference to code that javac checks, and it is called like any other method.

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 What does String.class.getRecordComponents() return?

    Choose one answer.

    Show the answer to question 1

    Answer: null, because String is not a record

    getRecordComponents returns the components of a record class in declaration order, and null for any class that is not a record, as the last line of the recorded run shows. Check isRecord() first, or handle the null.

  2. Question 2 of 5 A method called through Method.invoke throws an IllegalArgumentException of its own. What does the code that called invoke receive?

    Choose one answer.

    Show the answer to question 2

    Answer: An InvocationTargetException, whose cause is the IllegalArgumentException

    Method.invoke wraps any exception thrown by the method it calls in an InvocationTargetException, so the caller must look at getCause(). A method handle throws the method's own exception as it is.

  3. Question 3 of 5 handle has the type (Fare,int)int. What happens when this line runs?

    Read the code, then choose one answer.

    long result = (long) handle.invokeExact(fare, 18);
    Show the answer to question 3

    Answer: It throws WrongMethodTypeException, because the call site asks for a long and the handle returns an int

    invokeExact requires the types at the call site, including the cast on the result, to match the handle's type exactly. The recorded run shows the WrongMethodTypeException; handle.invoke would have converted the result.

  4. Question 4 of 5 Which call returns the private fields that a class declares itself?

    Choose one answer.

    Show the answer to question 4

    Answer: getDeclaredFields()

    The getDeclared… methods return what the class itself declares, private members included, but nothing inherited. getFields() returns public fields only, including inherited ones.

  5. Question 5 of 5 Many threads must add to a counter field in each of millions of small objects, without a lock and without a separate object per counter. Which tool fits?

    Choose one answer.

    Show the answer to question 5

    Answer: A VarHandle for the field, with getAndAdd

    A VarHandle gives an ordinary field atomic access modes such as getAndAdd and compareAndSet. Field.setInt and a setter handle are plain writes, which lose updates when threads race, and a lock is what the question rules out.

What reflection and method handles cost, measured with JMH Pro: opens with a Pro, Premium or Ultimate pass

The cost of a reflective call depends far more on where the Method or the handle is kept than on which API is used. This part measures seven ways of making the same call with JMH and turns the result into a cache, in an exercise.

In this part:

  • Seven ways to make one call, benchmarked
  • Reading the results
  • Caching per class with ClassValue
  • Exercise: a cached record-to-map converter

4 runnable examples (Checks.java, Java, Python, RecordMaps.java) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

What deep reflection may still do: atomic fields, open packages and final fields Pro: opens with a Pro, Premium or Ultimate pass

Deep reflection means reaching past private with setAccessible(true). This part shows the safe use of field access through a VarHandle, then the two limits that recent JDKs put on deep reflection, each reproduced on JDK 25 and JDK 27.

In this part:

  • Lost updates against VarHandle atomics, counted
  • Deep reflection into the JDK, refused and opened
  • Final fields: silent on JDK 25, a warning on JDK 27
  • A checklist for code that reaches past private

4 runnable examples (Java, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

  1. Fresher to mid · Warm-up

    What is reflection in Java, and why should application code use it sparingly?

    Reflection is the java.lang.reflect API that lets a running program inspect classes and use their members by name: list fields, methods, constructors and record components, read and write fields, call methods and create objects. Test runners, serializers and dependency-injection containers rely on it, because they must work with classes they never saw at compile time. Application code should use it sparingly because it gives up what the compiler checks: a misspelt name or a wrong argument type fails only at run time, wrapped in exceptions such as InvocationTargetException. Reflective code is also harder to read and to refactor, because tools cannot see the names it uses, and deep reflection into private members breaks encapsulation and is limited by the module system. When the operations are known in advance, an interface or a lambda does the same job with compile-time checks.

  2. Mid to senior

    After an upgrade from JDK 11 to JDK 25, a service fails at start-up with InaccessibleObjectException: "Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not opens java.lang to unnamed module". What does it tell you, and what are your options, from the quickest to the best?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Senior to staff

    A profile of a busy API shows 30 % of CPU time in an in-house reflection-based serializer. How would you make it faster, which changes would you measure first, and how would you prove the gain?

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    After moving a service to JDK 26, its logs show warnings that a final field "has been mutated reflectively" by a dependency. What is happening, what will break in later releases, and how do you plan for it?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Reflection finds and uses members by name at run time; getRecordComponents() lists a record’s components in order.
  • Method.invoke wraps failures in InvocationTargetException and checks argument types only when it runs.
  • Method handles check access once, at the lookup, and invokeExact demands the exact type.
  • VarHandles give a field atomic and ordered access modes, such as compareAndSet and getAndAdd.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.