The method handles API in the java.lang.invoke package is a powerful reflection and code generation API that has extensive JIT support as well. In this blog post I will give an overview of this API. I will go over some of the most commonly used parts of the API, but this is not a comprehensive guide. It’s meant to give you a starting point from which to start learning more on your own. The reader is encouraged to experiment with the samples on their own as well.

  1. What is a MethodHandle?
    1. Access checks
    2. Exception handling
    3. Signature polymorphism
    4. Handling the receiver
    5. The invokeExact method
    6. The invokeWithArguments method
  2. Method handle inlining
  3. Method handle combinators
    1. MethodHandles::insertArguments
    2. MethodHandles::filterArguments
    3. MethodHandles::collectArguments
    4. MethodHandles::permuteArguments
  4. Appendix A: MutableCallSite
  5. Appendix B: VarHandle

What is a MethodHandle?

A java.lang.invoke.MethodHandle is an object that wraps a fixed Java target method. It is, as the name says, a ‘handle’ for a Java method. The target method can be invoked through the method handle object.

In a lot of cases, we can use a functional interface to represent a reference to some invocable code, e.g. one of the interfaces in the java.util.function package. However, we can not rely on generics and functional interfaces to represent a method that takes a variable number of arguments with varying types. The best we can do is use a varargs method, which collects arguments into an array, but this incurs the overhead of the creation of the varargs array, the overhead of boxing and unboxing primitive values, and the overhead of storing and loading values from the array. We can also not represent both a method that returns a value and a method that returns nothing. There are workarounds using Void, but these still require a method to return some value, usually null.

interface GenericFunction<R> {
    R apply(Object... args);
}

static int m1(int x, int y) {
    return x + y;
}

static void m2() {}

static void main(String[] __) {
    GenericFunction<Integer> m2Ref = args -> { return m1((int) args[0], (int) args[1]); };
    int result = m2Ref.apply(1, 2); // arguments boxed into Object[]
    // result has to be unboxed

    // have to explicitly return 'null'
    GenericFunction<Void> m1Ref = args -> { m2(); return null; };
}

Method handles on the other hand don’t have the same limitations when it comes to boxing and unboxing (more on that later).

In practice, method handles are most useful to represent the result of an API that generates code at runtime, where the number and type of parameters is not known in advance. Method handles are for instance used to implement references to native functions in the foreign function and memory access (FFM) API. The method java.lang.foreign.Linker::downcallHandle which is used for this, accepts a FunctionDescriptor and returns a MethodHandle instance that can be used to invoke a C function. The linker API needs to provide access to any C function, so it needs a type that can be used to represent a reference to functions of varying arity, and with varying parameter and return types, while at the same time avoiding the overhead of varargs and boxing & unboxing primitve values. MethodHandle is an excellent choice in that case.

One of the simplest ways to create a MethodHandle for a particular Java method, is to perform a method handle lookup. First, we have to create a MethodHandles.Lookup object. This can be done using the MethodHandles::lookup factory method. Then, we can use one of the findXXX methods in Lookup to look up an existing Java method, as demonstrated by the following test program:

import java.lang.invoke.*;

public class Main {
    public static void main(String[] args) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodType typeOfTarget = MethodType.methodType(void.class);
        MethodHandle targetMh = lookup.findStatic(Main.class, "target", typeOfTarget);

        targetMh.invoke(); // prints 'invoking target'
    }

    public static void target() {
        System.out.println("invoking target");
    }
}

Here we have declared a static target method, which we can then look up with findStatic. We pass the class holding the target method, its name, and its type represented as a java.lang.invoke.MethodType, to findStatic. This gives us back a MethodHandle for the target method, which we then invoke by calling invoke. Note also that the invoke method throws a Throwable, so we handle that by declaring it in the throws clause of the main method.

The other findXXX methods in Lookup can be used to look up other kinds of methods. The findVirtual method corresponds to the invokevirtual bytecode, and can be used to look up instance (i.e. non-static) methods, also known as virtual methods. findConstructor can be used to look up constructors. There’s also find(Static)Getter and find(Static)Setter for reading and writing to fields. Note that these do not look up a getX or setX method, but rather look up a notional method that gets or sets a field directly. It is like a method to get or set the field is generated on the fly. These correspond the putfield, getfield, putstatic, and getstatic bytecodes. These are just a few examples.

You might have realized that a MethodHandle is very similar in concept to a java.lang.reflect.Method. However, the API is more minimal, as a method handle doesn’t reflect the access modifiers of the target method, or other things such as annotations. A method handle is really only 2 things: it has a type, and you can invoke it. The rest of the methods in MethodHandle are so-called combinators, which allow a client to create other method handles based on this one. We will discuss combinators in a later chapter.

Like j.l.r.Method, a method handle can be used, in the simplest of use cases, to reflectively invoke a Java target method. There are however also some key difference between the 2 APIs.

Access checks

The invoke method of a j.l.r.Method performs access checks when it is invoked it. This is also hinted at by the @CallerSensitive annotation. This informative annotation indicates that this method will inspect its caller when it is invoked. In practice, the invoke method will do a short stack walk to find the class of the caller (though this stack walk may be optimized away by the JIT). The method then checks whether the caller has the needed access privileges to invoke the target method.

A method handle on the other hand, doesn’t do any access checks when it is invoked (and therefore doesn’t need to be caller sensitive). Instead, the access checks are preformed when looking up the method handle. You’ll note that the MethodHandles::lookup method is caller sensitive instead. When calling lookup, the caller class is captured as the lookup class. This lookup class is then used when performing a method handle lookup to check that it has the needed access privileges to invoke the target method. This allows a client to do a single access check when the method handle is created, and after that get better performance by avoiding the access checks when invoking the method handle.

💡This also means that, if you have the method handle object, you have the capability to invoke it. Thus, we say that a method handle is a ‘capability’. Through sharing the method handle object, this capability can be shared with other code that might not normally have access to the target method, in order to allow that code to invoke the target method. This is one of the reasons why you might want to use method handles.

Exception handling

The invoke method of a j.l.r.Method wraps exceptions thrown by the underling Java method into an InvocationTargetException, while the invoke method of a MethodHandle will directly propagate the exception without wrapping. That is also why invoke throws a Throwable. This accounts for any possible throwable that needs to be propagated from the target method.

The fact that invoke throws a Throwable might seem cumbersome, since we need to deal with this Throwable somehow. However, if the target method does not declare any checked exceptions, we can assume that invoke will also never throw a checked exception in practice. So, in most cases we can just wrap the invoke call in a try/catch block that throws an unchecked exception in the catch block, such as:

MethodHandle mh = ...
try {
    mh.invoke();
} catch (Throwable t) {
    throw new RuntimeException("Should not happen", t);
}

💡The fact that invoke doesn’t wrap thrown exceptions also means that a method handle can be used to cleanly implement a method without having to worry about un-wrapping thrown exceptions in order to propagate them. This is another reason why you might want to use method handles.

Signature polymorphism

The invoke method of j.l.r.Method is declared as follows (some details omitted):

@CallerSensitive
public Object invoke(Object obj, Object... args)
    throws IllegalAccessException, InvocationTargetException

The first parameter represents the receiver instance of a virtual method call. It is ignored for static methods.

At first glance, the invoke method of MethodHandle may look very similar:

public final native @PolymorphicSignature Object invoke(Object... args) throws Throwable;

However, the return type and parameter type of this declaration are completely fictional. As the @PolymorphicSignature annotation implies, this method is signature polymorphic.

This method is so special, its even special-cased in the Java language specification. A signature polymorphic method is a method whose type is determined not by the method declaration, but by the method call site (the place in the code where the method is invoked). In other words, whatever argument types the code calling the invoke method passes to the call, those are the parameter types that the method will have. Since we can call invoke many times, passing different numbers and types of arguments, this also means that the invoke method essentially has many different types. In fact, the invoke method can take on any type that is declarable by a normal Java method declaration. The signature is thus: polymorphic. The same applies to the return type. Whatever type we cast the return value of the invoke method to, that is the return type the method will have.

Calling the j.l.r.Method::invoke method requires creating a new Object[], storing all the argument values into it, requiring us to box primitive values. The Object[] is then passed to the call, and the returned Object value has to be unboxed again if the value is a primitive. Signature polymorphism on the other hand, allows us to avoid all this overhead. Instead, all argument values are passed to, and returned from a sig-poly method as is.

This behavior is reified in the class file as well. The method type attached to the class file reference pointing to the MethodHandle::invoke method, is the exact (static) types of the argument values and return value, as parameter and return types. For example, if I have a piece of code like this:

MethodHandle mh = ...
String x = ...
int y = ...
long result = (long) mh.invoke(x, y);

Then the invokevirtual instruction that javac generates for this call to invoke, will point to the method type descriptor (Ljava/lang/String;I)J. This is different than the method type descriptor of the declaration of the invoke method, which would be ([Ljava/lang/Object;)Ljava/lang/Object;.

We can use the javap tool that comes with the JDK to check what type javac assigns to this call site as well. When disassembling the generated class file using javap -c <file>, we can find an invokevirtual instruction of the MethodHandle::invoke method with the type descriptor (Ljava/lang/String;I)J:

46: invokevirtual #44  // Method java/lang/invoke/MethodHandle.invoke:(Ljava/lang/String;I)J

You can read more about this in the JLS, for instance in section 15.12.3.

To make sig-poly methods work, the VM will generate some code every time a sig-poly method is linked, and this will happen once for each type that a sig-poly method takes on. This linking process involves spinning both machine code, and usually Java bytecode, to link the method handle call to the actual target method using the right invocation semantics, (depending on the target method e.g. static vs. virtual). Another way to think about it is that, a sig-poly method can have many different implementations. Though, these implementations are actually just small trampolines that forward the call to the actual target method that is described by the MethodHandle instance.

💡The fact that sig-poly methods accept arguments as-is, without needing to box them up into an Object[], is another good reason to use method handles. If you want a type that represents a piece of invocable code, but you aren’t sure what the exact types or number of arguments will be, then method handles are a good option. They provide a generic invocation API, without the inefficiency associated with boxing up arguments and return values.

Handling the receiver

The receiver is the this argument of a virtual method. j.l.r.Method handles the receiver as an explicit leading parameter of type Object:

Object invoke(Object obj, Object... args)

When invoking a static method, the obj argument is simply ignored. Typically we just pass null in place of the obj parameter.

For method handles on the other hand, the receiver parameter is just another parameter that is prepended to the parameter list. So, if we look up a virtual method:

public static void main(String[] args) throws Throwable {
    MethodHandle targetMh = MethodHandles.lookup().findVirtual(Main.class, "target",
            MethodType.methodType(void.class));

    targetMh.invoke(new Main()); // prints 'invoking target'
}

public void target() { // NOT static
    System.out.println("invoking target");
}

We need to pass an instance of the receiver (new Main()) when invoking the method handle. But, when we look up a static method handle (like in the first example in the ‘What is a MethodHandle’) there is no additional leading parameter.

Note in particular the method type that is used to look up the virtual method:

MethodType.methodType(void.class)

💡The method type used to look up a virtual method does not contain the receiver type. However, the receiver type does appear in the type of the returned method handle. When looking up virtual methods the receiver type is implied by the holder class (Main.class in this case). This is an important thing to keep in mind when looking up virtual methods.

The invokeExact method

Because a call to a sig-poly method passes the arguments as is, the bytecode generated for the call to invoke will not, for instance, automatically convert an argument of type int to long if that is the type of the parameter of the target method. However, the implementation of the invoke method takes care of that for us automatically. The type of the invoke method used at a call site does not have to match the type of the method handle. The implementation of the invoke method will convert all the arguments and the return value to the type that the target method expects.

The invokeExact method on the other hand, does not do any automatic argument conversions. Instead, the implementation of invokeExact will check that the type used at the call site matches the type of the method handle instance exactly. If there is a mismatch, a WrongMethodTypeException is thrown. When I say that the types need to match exactly, I really do mean exactly. If a method handle instance has a parameter of type Object, and I pass a value whose static type is String, I will get a WrongMethodTypeException. To get the right type at the call site, I would need to cast the argument value to object.

static void foo(Object o) {}

public static void main(String[] ___) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
            MethodType.methodType(void.class, Object.class));
    
    fooMh.invokeExact("Hello, world!");
}

This code throws:

java.lang.invoke.WrongMethodTypeException: handle's method type (Object)void but found (String)void

In order to make the call succeed, I have to cast the argument to Object, so that the call site has the exact same type as the method handle instance:

fooMh.invokeExact((Object) "Hello, world!");

The same goes for return values. If the return type of a method handle instance is not void, then, when using invokeExact, we have to cast the return value to the right type, which may require assigning the value even if it’s not used:

static int foo() { return 42; }

public static void main(String[] ___) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
            MethodType.methodType(int.class));
    
    int x = (int) fooMh.invokeExact(); // result is not used
}

So, why would we use invokeExact instead of invoke? Well, it turns out that automatically converting arguments to the right type is costly.

The implementation of invoke will call the MethodHandle::asType method. asType is our first example of a method handle combinator. The asType method accepts a MethodType and returns a new method handle with the given type, that does all the needed type adaptations, and then forwards the converted arguments to the original method handle. We can also call asType manually if we want:

static void foo(Object o) {}

public static void main(String[] ___) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
            MethodType.methodType(void.class, Object.class));

    // from (Object) -> void to (String) -> void
    fooMh = fooMh.asType(MethodType.methodType(void.class, String.class));

    fooMh.invokeExact("Hello, world!"); // this now works
}

To achieve this, the implementation of asType will generate a synthetic class + method that implements all the parameter and return type conversions. The result is then wrapped in a new method handle, which is returned. It is as if the asType implementation generated a little wrapper method like this:

static void foo(String str) {
    foo((Object) str);
}

If we call invoke on a method handle, the implementation will call asType to convert the type of the method handle to the type that is requested by the call site. It is probably easy to see how generating a synthetic class + method is costly when we just want to invoke a method. It’s not all bad though: each method handle instance has a 1 element cache, and the result of asType is stored in that cache. If the next call to asType requests the same method type again, the cached method handle is just returned. In practice this means that calling invoke is only costly if the same method handle instance is invoked multiple times with different call site types, because this causes repeated cache misses, and recreation of method handles with the requested type.

While very convenient, the automatic type conversions of invoke can also be a performance pitfall. However, we can be 100% sure to avoid this pitfall by using invokeExact, as it will never perform any type adaptations. So, as a performance rule of thumb: avoid inexact call. Prefer invokeExact over invoke.

💡When working with method handles, it is often useful to create a static wrapper function for calls to invokeExact:

static final MethodHandle FOO_MH = ...

public static void main(String[] ___) throws Throwable {
    fooWrapper("Hello, world!");
}

public static void fooWrapper(Object o) {
    FOO_MH.invokeExact(o);
}

Here the method fooWrapper has the exact same method type as the FOO_MH method handle. But, by wrapping it in a static method, we provide an non sig-poly method for clients to invoke instead. This means that e.g. the main method in the above example doesn’t have to worry about casting the string argument to Object explicitly, while at the same time avoiding any type mismatches between the method handle and the call site. We say that the fooWrapper ‘civilizes’ the sharp-but-flexible invokeExact API.

The invokeWithArguments method

Since j.l.r.Method::invoke accepts a vararg array of Object, we can not only pass in a list of arguments, which javac will automatically put into an Object[] for us, but we can also manually create an Object[] holding the argument values, that we than pass to j.l.r.Method::invoke directly:

Method m = ...
m.invoke(null, 1, 2, 3); // 1.) Ok
Object[] args = { 1, 2, 3 };
m.invoke(null, args); // 2.) also OK

This technique doesn’t work for sig-poly methods though. Because the type of a sig-poly method is derived from the call site, if we pass an Object[] when invoking a method handle, the type of the call site will just have Object[] as the parameter type. This array will not be automatically expanded into a list of arguments, since, again, arguments are passed to a sig-poly method as is.

So, if we wish to pass an Object[] or a List of argument values to a method handle, we need to use invokeWithArguments. This method will expand the array or list into a series of scalar argument values that are then passed to the method handle.

static void foo(int x, int y, int z) {}

public static void main(String[] __) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
            MethodType.methodType(void.class, int.class, int.class, int.class));
    
    fooMh.invoke(1, 2, 3); // OK
    Object[] args = { 1, 2, 3 };
    // fooMh.invoke(args); // BAD
    // WrongMethodTypeException: cannot convert MethodHandle(int,int,int)void to (Object[])void
    fooMh.invokeWithArguments(args); // Ok
}

Keep in mind though, that invokeWithArguments also does type adaptations internally, and similar performance caveats apply as to MethodHandle::invoke.

Method handle inlining

I will now discuss inlining optimizations that the C2 JIT compiler applies to method handle calls. This is a tricky topic, but important to get at least some idea of, in order to be able to use method handles effectively. There can be large performance discrepancies between different uses of method handles, and most of the time either inexact calls (see invokeExact section above) or lack of method handle inlining are to blame.

For regular Java method calls, we know which target method the call points at:

public static void foo() {}

public static void main(String[] args) {
    foo();
}

In the above, the reference to foo is embedded directly into the class file. This means that the JIT compiler knows exactly which method is being invoked, and can inline that method, which enables other optimizations to take place.

For method handles however, the information describing the target method is embedded in the method handle instance. So, when a method handle is invoked, we go through a trampoline that reads the target method from the method handle instance, and then forwards the call to that target. This indirection would normally prevent inlining the target method. After all, the receiver of a call to a method handle might be any arbitrary method handle instance, which can point at any arbitrary target method.

However, if the method handle is a constant, the JIT compiler can determine the target method at JIT-compile time, by inspecting the constant method handle instance, and treat a call to it as if it were a normal call to the target method, thus re-enabling inlining.

The easiest way to determine whether a value such as a MethodHandle is a constant, is to simply determine whether the value can be different for different invocations of the same code.

static final MethodHandle MH_FOO;

static {
    try {
        MH_FOO = MethodHandles.lookup().findStatic(Main.class, "foo",
                MethodType.methodType(void.class));
    } catch (ReflectiveOperationException e) {
        throw new ExceptionInInitializerError(e);
    }
}

static void foo() {}

static void m() throws Throwable {
    MH_FOO.invokeExact();
}

Here, the receiver of the invokeExact call is loaded directly from the static final field MH_FOO. static final fields can not be changed, not even through reflection. So, the JIT can constant fold the load from the MH_FOO field, making the receiver of invokeExact a constant. The JIT can then inspect the receiver’s target method and treat the call to invokeExact as a if it were a call to the target method foo.

We can check that inlining of the invokeExact call is taking place using the techniques described in chapter 3 of my other post about debugging JIT compilers: 3. Printing inlining traces.

If we apply those techniques to obtain an inlining trace for the method m in the above program, we get the following inlining trace:

@ 3   java.lang.invoke.LambdaForm$MH/0x00000236d7001400::invokeExact_MT (22 bytes)   force inline by annotation
  @ 10   java.lang.invoke.Invokers::checkExactType (17 bytes)   force inline by annotation
    @ 1   java.lang.invoke.MethodHandle::type (5 bytes)   accessor
  @ 14   java.lang.invoke.Invokers::checkCustomized (23 bytes)   force inline by annotation
    @ 1   java.lang.invoke.MethodHandleImpl::isCompileConstant (2 bytes)   (intrinsic)
  @ 18   java.lang.invoke.LambdaForm$DMH/0x00000236d7001800::invokeStatic (20 bytes)   force inline by annotation
    @ 8   java.lang.invoke.DirectMethodHandle::internalMemberName (8 bytes)   force inline by annotation
    @ 16   Main::foo (1 bytes)   inline (hot)

Here we can see at the end that the method Main::foo is being inlined.

Now for a more complex example:

...

static void m1() throws Throwable {
    m2(MH_FOO);
}

static void m2(MethodHandle mh) throws Throwable {
    mh.invokeExact();
}

Here the receiver instance on which we call invokeExact is not a constant inside the m2 method. After all, the argument passed to m2 can be an arbitrary method handle instance. If m2 were JIT compiled, no inlining of the invokeExact call could take place.

However, in the method m1, we pass the constant MH_FOO as an argument to m2. So, if the method m1 was JIT compiled, and in that compilation m2 was inlined, the receiver of invokeExact would be a constant, and inlining of the invokeExact call could take place.

Finally, lets look at instance fields:

class Widget {
    final MethodHandle mh_foo;

    ...

    void invoke() throws Throwable {
        mh_foo.invokeExact();
    }
}

Here, mh_foo is a field of an instance of Widget. If invoke was JIT compiled, we would get a load from the this instance to load the value of mh_foo. Since the this instance can be any instance of Widget, this load can not be constant folded. So, the receiver of invokeExact would also not be a constant, and we get no inlining.

static final Widget W = new Widget(MH_FOO);

static void m() throws Throwable {
    W.invoke();
}

In the method m, the receiver of invoke is a constant, since it’s loaded from the static final field W. So, if m is JIT compiled, and invoke is inlined, the this instance would be a constant. However, in that case, the load of the mh_foo field of Widget can still not be constant folded, since final fields are not ‘trusted’. They can for instance still be modified through reflection. As a result, the JIT may not constant fold the load of the mh_foo field, the receiver of invokeExact will not be constant, and the call can not be inlined.

We can see this in an inlining trace in the form of a call to MethodHandle::invokeBasic that failed to inline:

@ 18   java.lang.invoke.MethodHandle::invokeBasic()V (0 bytes)   failed to inline: receiver not constant

There are some exceptions to this rule though. Classes in certain packages in java.base have trusted final fields. Additionally, fields of a record are trusted as well. The JIT compiler may constant fold these fields if the enclosing instance is constant as well. So if we for instance turn Widget into a record:

record Widget(MethodHandle mh_foo) {
    void invoke() throws Throwable {
        mh_foo.invokeExact();
    }
}

And if we call invoke on a constant Widget instance, the JIT would be allowed to fold the load of the mh_foo field, and inlining of the invokeExact call could take place.

Method handle combinators

Method handle combinators are a large component of the method handle API. Most of these are found in the MethodHandles class as static methods, but some are also found as instance methods in the MethodHandle class itself.

Method handle combinators are a code generation API. Each method handle instance holds a small program called a LambdaForm, which describes what the method handle does. In most cases, this LambdaForm is rendered as bytecode that is then executed directly. The method handle combinator API can be used to create these little programs, wrapped up in MethodHandle instances.

As mentioned before, MethodHandle::asType is such a combinator. Let’s go over some other noteworthy examples.

The MethodHandles::insertArguments combinator

MethodHandles::insertArguments is probably one of the simplest combinators that you typically use. It allows a client to create a new method handle that passes one or more fixed argument values to a given method handle:

static void foo(int x, int y, int z) {
    System.out.println(STR."x=\{x} y=\{y} z=\{z}");
}

public static void main(String[] __) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
        MethodType.methodType(void.class, int.class, int.class, int.class));
    
    // at parameter index 1, insert argument value '2'
    MethodHandle mh = MethodHandles.insertArguments(fooMh, 1, 2);

    mh.invokeExact(1, 3); // prints 'x=1 y=2 z=3'
}

Here, we insert the fixed argument value 2 at index 1 in the parameter list of the downstream method handle fooMh. The result is a new method handle that takes just 2 int arguments. This is equivalent to writing a wrapper method for foo like so:

static void fooPrime(int x, int z) {
    foo(x, 2, z);
}

The MethodHandles::filterArguments combinator

Moving on, MethodHandles::filterArguments creates a method handle that pre-processes one or more of its arguments by calling a filter method, before passing on the result to a target method handle:

static void foo(int x, int y, int z) {
    System.out.println(STR."x=\{x} y=\{y} z=\{z}");
}

static int bar(int x) {
    return x + 1;
}

public static void main(String[] __) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
        MethodType.methodType(void.class, int.class, int.class, int.class));
    MethodHandle barMh = MethodHandles.lookup().findStatic(Main.class, "bar",
        MethodType.methodType(int.class, int.class));
    
    // applies filter 'barMh' to argument at index 1, and passes the result to 'fooMh'
    MethodHandle mh = MethodHandles.filterArguments(fooMh, 1, barMh);

    mh.invokeExact(1, 2, 3); // prints 'x=1 y=3 y=3'
}

This is equivalent to the following java code:

static void fooPrime(int x, int y, int z) {
    foo(x, bar(y), z);
}

Of note is that the filters used with filterArguments must have exactly 1 parameter, and return a value of the type that the downstream method handle accept at the filtered index.

The MethodHandles::collectArguments combinator

MethodHandles::collectArguments has less restrictions, as it allows multiple argument values to be condensed into one value passed to the downstream method handle:

static void foo(int x, int y, int z) {
    System.out.println(STR."x=\{x} y=\{y} z=\{z}");
}

static int bar(int x, int y) {
    return x + y;
}

public static void main(String[] __) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
        MethodType.methodType(void.class, int.class, int.class, int.class));
    MethodHandle barMh = MethodHandles.lookup().findStatic(Main.class, "bar",
        MethodType.methodType(int.class, int.class, int.class));
    
    // applies filter 'barMh' to argument at index 1, and passes the result to 'fooMh'
    MethodHandle mh = MethodHandles.collectArguments(fooMh, 1, barMh);

    mh.invokeExact(1, 2, 2, 3); // prints 'x=1 y=4 y=3'
}

This is equivalent to the following java code:

static void fooPrime(int a0, int a1, int a2, int a3) {
    foo(a0, bar(a1, a2), a3);
}

Alternatively, the collector may also accept zero arguments, in which case it could generate an argument value on the fly, like a supplier:

static void foo(int x, int y, int z) {
    System.out.println(STR."x=\{x} y=\{y} z=\{z}");
}

static int bar() {
    return ThreadLocalRandom.current().nextInt();
}

public static void main(String[] __) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
        MethodType.methodType(void.class, int.class, int.class, int.class));
    MethodHandle barMh = MethodHandles.lookup().findStatic(Main.class, "bar",
        MethodType.methodType(int.class));
    
    // applies filter 'barMh' to argument at index 1, and passes the result to 'fooMh'
    MethodHandle mh = MethodHandles.collectArguments(fooMh, 1, barMh);

    mh.invokeExact(1, 3); // prints 'x=1 y=<random numer> y=3'
}

This is still pretty straightforward, but collectArguments can also be used together with a collector that returns void. If we do that, the collector essentially acts as a side effect that is executed before the downstream method handle, rather than computing an argument of the downstream handle based on zero or more inputs.

In the case of a void-returning collector, the index we specify to collectArguments indicates the place in the parameter list of the downstream handle where we want to insert the parameter list of the collector. Instead of this index pointing at a particular parameter of the downstream handle, it can be thought of as pointing at the space between parameters. Where, e.g. index 0 would indicate that the parameters of the collector should appear before the first parameter of the downstream handle. For instance:

static void foo(String s, Object o, int i) {
}

static void bar(long l, double d) {
}

public static void main(String[] __) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
        MethodType.methodType(void.class, String.class, Object.class, int.class));
    MethodHandle barMh = MethodHandles.lookup().findStatic(Main.class, "bar",
        MethodType.methodType(void.class, long.class, double.class));

    MethodHandle mh = MethodHandles.collectArguments(fooMh, 0, barMh);
    System.out.println(mh.type()); // prints '(long,double,String,Object,int)void'
}

The type of mh here is (long,double,String,Object,int)void.

And of course, we could even have a collector that takes no arguments and returns nothing. collectArguments is probably the most flexible combinator out there.

The MethodHandles::permuteArguments combinator

MethodHandles::permuteArguments can be used to change the order of the parameters of a method handle, or to duplicate certain argument values and “broadcast” them to multiple parameters of the downstream method handle:

static void foo(int a0, int a1, int a2, int a3) {
    System.out.println(STR."a0=\{a0} a1=\{a1} a2=\{a2} a3=\{a3}");
}

public static void main(String[] __) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
        MethodType.methodType(void.class, int.class, int.class, int.class, int.class));

    MethodType newType = MethodType.methodType(void.class, int.class, int.class, int.class);
    int[] reorder = { 1, 0, 2, 2 };
    MethodHandle mh = MethodHandles.permuteArguments(fooMh, newType, reorder);

    mh.invokeExact(1, 2, 3); // prints 'a0=2 a1=1 a2=3 a3=3'
}

This is equivalent to the following java code:

static void fooPrime(int a0, int a1, int a2) {
    foo(a1, a0, a2, a2);
}

permuteArguments accepts a method type describing the type of the notional wrapper method, and a ‘reorder array’, which describes how each parameter in the new type is wired to a parameter of the target method handle (fooMh).

According to the reorder array: the first parameter of fooMh should receive the argument at index 1, the second parameter of fooMh should receive the argument at index 0, and the 3rd and 4th parameters of fooMh should both receive the argument at index 2.

The argument duplication capability of permuteArguments essentially allows you to use an argument value multiple times in a downstream method handle. It can even be used to drop argument values that are not needed by the downstream method handle, by specifying a new method type that has more arguments than the downstream handle, and a reorder array that doesn’t use every parameter index:

static void foo(int a0, int a1) {
    System.out.println(STR."a0=\{a0} a1=\{a1}");
}

public static void main(String[] __) throws Throwable {
    MethodHandle fooMh = MethodHandles.lookup().findStatic(Main.class, "foo",
        MethodType.methodType(void.class, int.class, int.class));

    MethodType newType = MethodType.methodType(void.class, int.class, int.class, int.class);
    int[] reorder = { 0, 1 };
    MethodHandle mh = MethodHandles.permuteArguments(fooMh, newType, reorder);

    mh.invokeExact(1, 2, 3); // prints 'a0=1 a1=2'
}

However, the MethodHandles::dropArguments combinator might be more convenient for that use case.

Other combinators

There are many more combinators found in the MethodHandles and MethodHandle classes, but I won’t go over all of them here. I’ve gone over some of the commonly used combinators, to give the basic gist of how combinators work and can be reasoned about, but ultimately, the best way to learn all the combinators is to experiment with them yourself.

💡As we’ve seen, method handle combinators make it very easy to generate small snippets of code. The combinators are essentially an API to programmatically write Java code. Combinators are yet another good reason why you’d want to use method handles.

Appendix A: MutableCallSite

MutableCallSite is another noteworthy class in the java.lang.invoke package. It is essentially just a holder for a target method handle. However, the special thing is that, even though the target field is mutable, the JIT may constant fold loads from this field (as long as the enclosing MutableCallSite instance is constant as well).

This means that you can essentially have a ‘mostly-constant’ method handle, that can still take advantage of method handle inlining optimizations, but which can also be be swapped out for another method handle. To make this work, the JIT will record a ‘dependency’ on the state of the mutable call site in compiled code, and when the call site’s target is changed, the compiled code will be thrown away. This is a powerful tool, but should be used sparingly, since the code will go back to running in the interpreter, and will have to be compiled again by the JIT.

Appendix B: VarHandle

Var handles can be thought of as bundles of method handles that relate specifically to memory access. Instead of a single invoke method they have various get*/set* methods that implement memory access with different memory ordering semantics.

The same performance caveats apply to var handles as to method handles. The receiver VarHandle instance of a get*/set* call needs to be a constant, and the call needs to be exact. Note though, that while method handles have a dedicated invokeExact method, var handles don’t. The exact invocation behavior of var handles has to be turned on explicitly by calling VarHandle::withInvokeExactBehavior. This method returns a new var handle who’s get*/set* will check that the call site type matches the type of the corresponding var handle’s access mode.