David Lloyd - JVM engineer, contributor to more Java infrastructure than most of us will ever touch - is writing a series on the new reflection API, and the intermediate installment is genuinely worth reading. It covers the MethodHandles.Lookup factory methods in real depth, walks through adapters and combinators, and has a clean API design section on why you treat handles like capabilities rather than conveniences.
What it doesn't cover is when you'd reach for any of this and why you'd bother. And its main problem, apart from the reasoning David doesn't go into, is that David's busy and takes too long to write new material for us to read!
You've Already Used MethodHandles
The most important thing to know about MethodHandle is that you use it every time you write a lambda.
When the Java compiler sees a lambda, it doesn't generate an anonymous class. It emits an invokedynamic instruction, and at runtime the JVM uses a MethodHandle under the hood - specifically through LambdaMetafactory - to wire up the implementation. The first time the lambda site is hit, the JVM does the work of creating a functional interface implementation backed by a method handle. After that, the JIT sees through it and inlines aggressively.
This matters for two reasons. First, MethodHandles aren't some exotic corner of the API - they're the mechanism the language itself uses for one of its most common abstractions. Second, if the JVM can use them to make lambdas fast, you can use the same machinery to make your own dynamic dispatch fast.
The Problem with Method.invoke
The standard argument for MethodHandles over traditional reflection is performance, and it's a real argument — but it's worth being specific about where the cost comes from.
Traditional reflection looks like this:
// Done once — fine
Method method = String.class.getMethod("toUpperCase");
// Done thousands of times — not fine
for (String s : hugeList) {
String result = (String) method.invoke(s);
// access check on every call
}
Every call to Method.invoke performs an access check. Is the caller allowed to invoke this method? Is it public? Does the module system permit it? These checks aren't free, and in hot paths - a DI container wiring up beans, a serializer walking object graphs, an ORM mapping rows to fields - they add up fast.
The MethodHandle equivalent moves that check to creation time:
// Access check happens here, once
private static final MethodHandle TO_UPPER;
static {
try {
TO_UPPER = MethodHandles.lookup()
.findVirtual(String.class,
"toUpperCase",
MethodType.methodType(String.class));
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new ExceptionInInitializerError(e);
}
}
// And never again
for (String s : hugeList) {
String result = (String) TO_UPPER.invokeExact(s);
}
Once you have a handle, it's an unconditional capability. The JIT can treat a static final MethodHandle as a constant and inline through it - something it explicitly cannot do with Method.invoke. This is the same reason the JVM uses handles for lambda dispatch.
The practical implication: in framework code, look up your handles once during initialization, store them in static final fields, and invoke them freely. The cost of the lookup is paid once; all subsequent invocations are cheap.
Currying, Without Calling It That
David's article shows bindTo and insertArguments as handle adapters. What it doesn't say is that these are currying and partial application - the functional programming concepts, applied to Java's reflection layer.
insertArguments lets you fix one or more arguments at any position, producing a new handle with a reduced signature:
MethodHandle compare = lookup.findStatic(
Integer.class,
"compare",
MethodType.methodType(int.class,
int.class,
int.class));
// compare: (int, int) -> int
MethodHandle compareToZero = MethodHandles
.insertArguments(compare, 1, 0);
// compareToZero: (int) -> int — second argument permanently fixed to 0
The practical use: you construct one expensive base handle and produce cheap, specialized variants from it: a comparator factory that takes a key extractor and produces a Comparator implementation; a formatter that takes a locale at configuration time and produces a handle that formats values without re-resolving the locale on every call. Strategy objects that you used to build with anonymous classes or lambdas, now built by binding arguments to handles - and storable, composable, and inspectable in ways that lambdas aren't.
This is genuinely useful in rules engines and data pipeline code where you're assembling behavior from parts at runtime and want to do it without the overhead of reflection on every execution.
The JPMS Access Problem
setAccessible is how Java developers have been breaking into private APIs since Java 1.1. The module system has been making that progressively harder since Java 9, and intentionally so: setAccessible across module boundaries now requires explicit opens declarations, and the JDK itself has been closing its own hatches.
The Lookup-as-capability pattern is the sanctioned replacement, and it's cleaner than setAccessible in ways that matter.
The idea: instead of your framework forcing its way into a caller's module, the caller passes you a Lookup object that carries their access rights. You use that lookup to find the handles you need, then discard it. The access was granted by the module that owns the code, not seized from outside.
// Framework entry point — caller passes in their Lookup
public void register(MethodHandles.Lookup callerLookup,
Class<?> type) throws Exception {
MethodHandle constructor = callerLookup.findConstructor(type,
MethodType.methodType(void.class));
// Use the handle; discard the lookup
}
The difference from setAccessibleis that the module system enforces that access, and the intent is explicit. The caller is handing you a key, not you picking a lock. David's post touches this in the API design section; the fuller story is that this is increasingly how you're supposed to write frameworks as JPMS becomes harder to route around.
LambdaMetafactory: Adapting Handles into Typed Interfaces
The most powerful and least-used application of MethodHandles is LambdaMetafactory - the same mechanism the compiler uses for lambdas, available directly.
The idea is that you have a MethodHandle pointing at some dynamically-resolved method, and you want to call it through a typed functional interface with zero per-call overhead. LambdaMetafactory will generate an implementation class for you at runtime, just as javac does for your lambdas.
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle handle = lookup.findVirtual(String.class,
"toUpperCase",
MethodType.methodType(String.class));
// Adapt it to Function<String, String>
CallSite site = LambdaMetafactory.metafactory(
lookup,
"apply",
MethodType.methodType(Function.class),
MethodType.methodType(Object.class, Object.class), // erased type
handle,
MethodType.methodType(String.class, String.class) // actual type
);
Function<String, String> fn = (Function<String, String>) site
.getTarget()
.invokeExact();
fn.apply("hello"); // returns "HELLO", JIT-inlinable, no reflection overhead
This is overkill for most application code. It's not overkill if you're writing a serialization library that needs to call getters and setters at maximum speed, or a DI container that needs to invoke constructors on hot paths, or any framework where dynamic dispatch is unavoidable but the overhead of Method.invoke is not.
David writes a lot of very low-level code that wants this kind of performance. He's speaking from experience.
The result is a typed functional interface you can pass around like any other, with the JIT able to see through it - the same thing the compiler gives you for free when you write a lambda, earned manually when your target is dynamic.
When to Actually Reach for This
None of this is advice to go rewrite your application code with MethodHandle. Strong, direct calls are faster than anything here, and your DI container is almost certainly already abstracting away the performance cost for you.
The cases where MethodHandles are the right tool:
- You're writing infrastructure code - a framework, a library, a serializer - where dynamic dispatch is unavoidable and you control the hot path and you've measured call costs
- You're replacing a pattern that uses
Method.invokein a loop and, again, the access checks are measurable - You need to do something the module system would block with
setAccessible, and aLookup-passing pattern is the right way to get access legally - You're composing behavior at runtime from parts - currying, partial application, building strategies from components - and you want something more structured and performant than a chain of lambdas
For the mechanical depth - how lookup works, how the adapters compose, how the type system threads through all of it - David's series is the right reference. The intermediate post is where to go when you're ready to write the code. The first post covers the fundamentals if you need the foundation first.