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.
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:
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());
}
} 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:
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)));
}
} 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
Three ways from a class to a call
Text description of the diagram
The diagram is a column of four boxes joined by arrows.
- The class, Class<Fare>, found by name or with Fare.class.
- 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.
- 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.
- 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.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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).
-
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.
-
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?
-
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?
-
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?
Key takeaways
- Reflection finds and uses members by name at run time;
getRecordComponents()lists a record’s components in order. Method.invokewraps failures inInvocationTargetExceptionand checks argument types only when it runs.- Method handles check access once, at the lookup, and
invokeExactdemands the exact type. - VarHandles give a field atomic and ordered access modes, such as
compareAndSetandgetAndAdd.
References
- java.lang.reflect (Java SE 25 API) (Oracle)
- Class (Java SE 25 API) (Oracle)
- RecordComponent (Java SE 25 API) (Oracle)
- Method (Java SE 25 API) (Oracle)
- MethodHandle (Java SE 25 API) (Oracle)
- MethodHandles (Java SE 25 API) (Oracle)
- VarHandle (Java SE 25 API) (Oracle)
- ClassValue (Java SE 25 API) (Oracle)
- JEP 416: Reimplement Core Reflection with Method Handles (OpenJDK)
- JEP 403: Strongly Encapsulate JDK Internals (OpenJDK)
- JEP 500: Prepare to Make Final Mean Final (OpenJDK)
- JEP 193: Variable Handles (OpenJDK)
- The java Command (JDK 25 tool reference) (Oracle)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress