Contents

Method handles are obtained from a MethodHandles.Lookup object, which carries the access rights of the class where it was created. Access control is checked once at lookup time, not on every invocation.

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

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 Prefer invokeExact in hot code paths — it is the fastest invocation and can be fully inlined by the JIT. Use invoke or invokeWithArguments when types are not statically known or for convenience in less-critical code.

MethodHandles provides combinator methods to adapt and transform handles:

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)

You can wrap existing java.lang.reflect objects into method handles using unreflect methods. This is useful when you already have a Method, Field, or Constructor from reflection:

// 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));

MethodHandles are preferred over reflection in performance-critical code because the JIT compiler can inline and optimize them in ways that Method.invoke() cannot. They are the ideal foundation for implementing dynamic dispatch — building lookup tables of handlers keyed by type at startup, then invoking them on the hot path with near-zero overhead. Internally they power many core Java mechanisms: invokedynamic byte code, the lambda metafactory, and framework internals such as method routers and property accessors.

// 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); } } Method handles checked at lookup time but types verified at invocation — a WrongMethodTypeException (unchecked) is thrown at runtime if types don't match. Always store handles in static final fields and compute them once during class initialization for maximum JIT optimization.