Java MethodHandles: JVM-Native Method Invocation
MethodHandles, introduced in Java 7, provide a lower-level, faster alternative to java.lang.reflect. A MethodHandle is a typed, directly executable reference to a method, constructor, or field: it can be inlined by the JIT compiler in ways that reflection cannot. MethodHandles are the backbone of invokedynamic, lambdas, and VarHandle internally, and they're essential for framework authors and anyone building high-performance reflective code.
Contents
Lookup and Finding Methods
Method handles are obtained from a MethodHandles.Lookup
import java.lang.invoke.*;
// Lookup has the access rights of the calling class
MethodHandles.Lookup lookup = MethodHandles.lookup();
// findVirtual — instance methods (dispatch based on receiver type)
MethodHandle toUpperCase = lookup.findVirtual(
String.class, "toUpperCase", MethodType.methodType(String.class));
// MethodType: (return type, param types...)
// For instance methods, receiver type is the first argument at invocation
// findStatic — static methods
MethodHandle parseInt = lookup.findStatic(
Integer.class, "parseInt", MethodType.methodType(int.class, String.class));
// findConstructor — constructors
MethodHandle newStringBuilder = lookup.findConstructor(
StringBuilder.class, MethodType.methodType(void.class, String.class));
// findGetter / findSetter — field access (same as get/set field in reflection)
MethodHandle nameGetter = lookup.findGetter(Person.class, "name", String.class);
MethodHandle nameSetter = lookup.findSetter(Person.class, "name", String.class);
// findSpecial — super method calls (like invokespecial bytecode)
// Only accessible from the class doing the call
MethodHandle superToString = lookup.findSpecial(
Object.class, "toString",
MethodType.methodType(String.class), MyClass.class);
Invoking: invokeExact vs invoke
There are two invocation styles with very different type-checking semantics:
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle toUpperCase = lookup.findVirtual(
String.class, "toUpperCase", MethodType.methodType(String.class));
// invokeExact — types must match EXACTLY (fastest, JIT-inlineable)
String result = (String) toUpperCase.invokeExact("hello");
// ^ Works: receiver is String, return cast to String
// invokeExact will throw WrongMethodTypeException if types don't match:
// Object result2 = (Object) toUpperCase.invokeExact("hello"); // WRONG
// Must be: String result2 = (String) toUpperCase.invokeExact("hello");
// invoke — allows auto-conversion (slower, more flexible)
Object result3 = toUpperCase.invoke("hello"); // auto-unboxes/casts
String result4 = (String) toUpperCase.invoke("hello");
// invokeWithArguments — takes a List or varargs (most flexible, slowest)
String result5 = (String) toUpperCase.invokeWithArguments(List.of("hello"));
// Calling a static method handle
MethodHandle parseInt = lookup.findStatic(
Integer.class, "parseInt", MethodType.methodType(int.class, String.class));
int n = (int) parseInt.invokeExact("42"); // exactly matches the MethodType
// Calling a constructor
MethodHandle newSB = lookup.findConstructor(
StringBuilder.class, MethodType.methodType(void.class, String.class));
StringBuilder sb = (StringBuilder) newSB.invoke("hello");
System.out.println(sb.toString()); // hello
invokeExactinvokeinvokeWithArgumentsMethod Handle Transformations
MethodHandles
MethodHandles.Lookup lookup = MethodHandles.lookup();
// bindTo — partially apply (bind receiver or first argument)
MethodHandle toUpperCase = lookup.findVirtual(
String.class, "toUpperCase", MethodType.methodType(String.class));
MethodHandle helloUpper = toUpperCase.bindTo("hello");
// Now takes no arguments, always returns "HELLO"
String s = (String) helloUpper.invokeExact(); // "HELLO"
// asType — adapt the MethodType (with conversions)
MethodHandle parseInt = lookup.findStatic(
Integer.class, "parseInt", MethodType.methodType(int.class, String.class));
MethodHandle parseIntObj = parseInt.asType(
MethodType.methodType(Object.class, Object.class));
// Now accepts Object and returns Object (with auto-boxing/casting)
// dropArguments — insert ignored arguments at given positions
MethodHandle upper = lookup.findVirtual(String.class, "toUpperCase",
MethodType.methodType(String.class));
MethodHandle upperIgnoreInt = MethodHandles.dropArguments(
upper, 1, int.class); // now: (String, int) → String (ignores the int)
// insertArguments — partially apply by inserting fixed arguments
MethodHandle substring = lookup.findVirtual(
String.class, "substring", MethodType.methodType(String.class, int.class, int.class));
MethodHandle sub0to3 = MethodHandles.insertArguments(substring, 1, 0, 3);
// now: (String) → String, always calls substring(0, 3)
String prefix = (String) sub0to3.invokeExact("hello world"); // "hel"
// filterArguments — apply a handle to transform arguments before passing
MethodHandle trimmer = lookup.findVirtual(String.class, "trim",
MethodType.methodType(String.class));
MethodHandle filteredUpper = MethodHandles.filterArguments(upper, 0, trimmer);
// trimmer.invoke(s) is called first, then upper.invoke(trimmed)
String r = (String) filteredUpper.invokeExact(" hello "); // "HELLO"
// guardWithTest — conditional dispatch (if pred then trueHandle else falseHandle)
MethodHandle isNull = lookup.findStatic(
Objects.class, "isNull", MethodType.methodType(boolean.class, Object.class));
// MethodHandles.guardWithTest(test, trueHandle, falseHandle)
Unreflect: From java.lang.reflect
You can wrap existing java.lang.reflectunreflectMethodFieldConstructor
// unreflect — wrap a java.lang.reflect.Method
Method method = String.class.getMethod("toUpperCase");
MethodHandle mh = MethodHandles.lookup().unreflect(method);
String result = (String) mh.invoke("hello"); // "HELLO"
// unreflectConstructor — wrap a Constructor
Constructor<ArrayList> ctor = ArrayList.class.getConstructor(int.class);
MethodHandle newList = MethodHandles.lookup().unreflectConstructor(ctor);
List<String> list = (List<String>) newList.invoke(16); // new ArrayList(16)
// unreflectGetter / unreflectSetter — wrap a Field
Field nameField = Person.class.getDeclaredField("name");
nameField.setAccessible(true);
MethodHandle getter = MethodHandles.lookup().unreflectGetter(nameField);
MethodHandle setter = MethodHandles.lookup().unreflectSetter(nameField);
Person p = new Person("Alice");
String name = (String) getter.invoke(p); // "Alice"
setter.invoke(p, "Bob"); // sets name to "Bob"
// privateLookupIn — access private members of another class (Java 9+)
// Requires the other class to open its package to your module
MethodHandles.Lookup privateLookup =
MethodHandles.privateLookupIn(OtherClass.class, MethodHandles.lookup());
MethodHandle privateMethod = privateLookup.findVirtual(
OtherClass.class, "privateMethod", MethodType.methodType(void.class));
Practical Use Cases
MethodHandleMethod.invoke()invokedynamic
// Use case 1: High-performance event dispatcher
// Build a map of method handles indexed by event type at startup
Map<Class<?>, MethodHandle> handlers = new HashMap<>();
MethodHandles.Lookup lookup = MethodHandles.lookup();
// Register handlers by scanning annotations (once, at startup)
for (Method m : EventHandler.class.getMethods()) {
if (m.isAnnotationPresent(Handle.class)) {
Class<?> eventType = m.getParameterTypes()[0];
handlers.put(eventType, lookup.unreflect(m)
.bindTo(new EventHandler())); // bind receiver
}
}
// Dispatch (hot path — invokeExact is JIT-inlined)
void dispatch(Object event) throws Throwable {
MethodHandle handler = handlers.get(event.getClass());
if (handler != null) handler.invoke(event);
}
// Use case 2: Generic accessor without reflection overhead
class BeanAccessor {
private final MethodHandle getter;
private final MethodHandle setter;
BeanAccessor(Class<?> beanClass, String fieldName, Class<?> fieldType)
throws ReflectiveOperationException {
var lookup = MethodHandles.lookup();
this.getter = lookup.findGetter(beanClass, fieldName, fieldType);
this.setter = lookup.findSetter(beanClass, fieldName, fieldType);
}
Object get(Object bean) throws Throwable { return getter.invoke(bean); }
void set(Object bean, Object value) throws Throwable { setter.invoke(bean, value); }
}
WrongMethodTypeExceptionstatic final