The new reflection (a 4-part series)

Recap

In the previous post, we covered the lookup factory methods, basic adaptations like bindTo and insertArguments, and some principles of API design with method handles.

In this post, we'll explore the more advanced combinators that let you compose method handles into pipelines of behavior: transforming arguments and return values, branching on conditions, handling exceptions, and more.

Composing method handles

The MethodHandles class provides a family of static methods that combine one or more method handles into a new one. These are the method handle equivalent of function composition, and they're what turn method handles from a simple reflection replacement into a genuinely powerful abstraction.

Transforming arguments with filterArguments

MethodHandles.filterArguments lets you transform one or more arguments before they reach the target handle. Each filter is a unary handle: it takes one argument and returns a transformed value that replaces the original argument at its position.

// Target: a method that takes an int
// Filter: Integer.parseInt(String) -> int
// Result: a method that takes a String (which gets parsed to int first)

MethodHandle target = ...;  // type (int)void
MethodHandle filter = lookup.findStatic(Integer.class, "parseInt",
    MethodType.methodType(int.class, String.class));

MethodHandle filtered = MethodHandles.filterArguments(target, 0, filter);
// filtered has type (String)void
filtered.invokeExact("42");  // calls target with (int) 42

The second parameter is the position at which to start applying filters. You can supply multiple filters to transform consecutive arguments.

Transforming the return value with filterReturnValue

MethodHandles.filterReturnValue applies a transformation to the result after the target handle completes. The filter handle must accept exactly the return type of the target and may return any type.

// Target: String.length() -> int
// Filter: a handle that boxes an int to Integer
// Result: String -> Integer

MethodHandle length = lookup.findVirtual(String.class, "length",
    MethodType.methodType(int.class));
MethodHandle box = lookup.findStatic(Integer.class, "valueOf",
    MethodType.methodType(Integer.class, int.class));

MethodHandle boxedLength = MethodHandles.filterReturnValue(length, box);
// boxedLength has type (String)Integer
Integer len = (Integer) boxedLength.invokeExact("hello"); // returns (Integer) 5

Computing a prefix value with foldArguments

MethodHandles.foldArguments is a bit more subtle. It calls a combiner handle on some prefix of the arguments, and then calls the target handle with the combiner's return value prepended to the original arguments.

This is useful when you need to compute a value from the arguments and pass both the computed value and the original arguments to the target.

// Combiner: computes a hash code from a String
// Target: takes (int hashCode, String value) and does something with both

MethodHandle combiner = lookup.findVirtual(String.class, "hashCode",
    MethodType.methodType(int.class));
// combiner has type (String)int

MethodHandle target = ...;  // type (int, String)void

MethodHandle folded = MethodHandles.foldArguments(target, combiner);
// folded has type (String)void
// When called: combiner runs first on the String, producing an int;
// then target is called with (int, String)

Collecting arguments with collectArguments

MethodHandles.collectArguments is a generalization that replaces a single argument of the target at a given position with the result of calling a collector handle on some arguments. Unlike foldArguments, the collector's arguments are consumed and do not appear in the resulting handle's type.

// Target: takes (String, int)void
// Collector at position 1: Integer.parseInt(String) -> int
// Result: takes (String, String)void - the second String is parsed to int

MethodHandle collected = MethodHandles.collectArguments(target, 1, parseInt);
// When called with ("hello", "42"):
//   parseInt("42") -> 42
//   target("hello", 42)

Spreading and collecting arrays

Sometimes you need to bridge between array-based and positional argument passing.

Spreading: array to positional

MethodHandle.asSpreader converts a handle that takes N positional arguments into one that takes an array and spreads it across those positions:

MethodHandle compare = lookup.findStatic(Integer.class, "compare",
    MethodType.methodType(int.class, int.class, int.class));
// compare has type (int, int)int

MethodHandle spread = compare.asSpreader(int[].class, 2);
// spread has type (int[])int

int result = (int) spread.invokeExact(new int[] { 3, 7 }); // returns -1

Collecting: positional to array

MethodHandle.asCollector does the reverse: trailing positional arguments are gathered into an array before calling the target.

MethodHandle target = ...;  // type (String, Object[])void

MethodHandle collecting = target.asCollector(Object[].class, 3);
// collecting has type (String, Object, Object, Object)void

The related asVarargsCollector goes a step further, making the handle behave like a varargs method: it will accept any number of trailing arguments and automatically collect them into an array.

Control flow combinators

One of the most powerful aspects of method handles is the ability to express control flow - branching, exception handling, and cleanup - as composed method handles rather than wrapper methods.

Conditional execution with guardWithTest

MethodHandles.guardWithTest is the method handle equivalent of if/else. It takes three handles:

  • test: a handle returning boolean
  • target: called when the test returns true
  • fallback: called when the test returns false

All three must accept the same argument types. The test's arguments are passed to whichever branch is selected.

MethodHandle isNull = ...;   // type (Object)boolean
MethodHandle toString = ...; // type (Object)String
MethodHandle fallback = MethodHandles.constant(String.class, "null");
fallback = MethodHandles.dropArguments(fallback, 0, Object.class);

MethodHandle safeToString = MethodHandles.guardWithTest(isNull, fallback, toString);
// safeToString has type (Object)String

Exception handling with catchException

MethodHandles.catchException wraps a target handle in a try/catch. If the target throws an exception of the specified type, the handler is called instead. The handler receives the caught exception as its first argument, followed by the original arguments.

MethodHandle risky = ...;   // type (String)int, might throw NumberFormatException
MethodHandle handler = ...; // type (NumberFormatException, String)int

MethodHandle safe = MethodHandles.catchException(risky,
    NumberFormatException.class, handler);
// safe has type (String)int - exception is caught and handled

Cleanup with tryFinally

MethodHandles.tryFinally ensures that a cleanup handle runs after the target, regardless of whether it completes normally or throws. The cleanup handle receives the thrown exception (or null), the result (or zero), and the original arguments.

MethodHandle acquire = ...;  // type ()Resource
MethodHandle cleanup = ...;  // type (Throwable, Resource)void - null Throwable on success

MethodHandle managed = MethodHandles.tryFinally(acquire, cleanup);

ConstantBootstraps

The ConstantBootstraps class provides bootstrap methods originally intended for use with constant dynamic (condy) entries in class files. However, several of its methods are useful to call directly because they wrap common operations in a way that avoids checked exceptions.

Avoiding checked exceptions with fieldVarHandle

The standard way to acquire a VarHandle for a field via Lookup requires catching NoSuchFieldException and IllegalAccessException. The ConstantBootstraps.fieldVarHandle method wraps this into a single call that throws only unchecked exceptions, making it ideal for inline field initialization:

private static final VarHandle stateHandle = ConstantBootstraps.fieldVarHandle(
    MethodHandles.lookup(),
    "state",        // field name
    VarHandle.class, // the type of the constant being produced
    MyClass.class,  // declaring class
    int.class       // field type
);

We will explore VarHandle in detail in the next post, but the convenience of this pattern is worth noting here since it eliminates the need for a static initializer block with try/catch.

Other useful bootstraps

  • getStaticFinal reads a static final field from a class, which is particularly useful for accessing constants like BigInteger.ZERO or Boolean.TRUE in a bootstrap context.
  • enumConstant looks up an enum constant by name, returning it as the appropriate enum type.
  • invoke calls an arbitrary MethodHandle with given arguments and returns the result: useful for computing derived constants.

Performance considerations

Method handles can be highly optimized by the JIT compiler, but this optimization depends on a few key practices.

Store on static final fields

When a MethodHandle is stored on a private static final field, it is possible that the JIT can treat it as a compile-time constant. This enables constant folding: the JIT can see exactly what the handle does and potentially inline the entire chain of adaptations and compositions into the call site. This is the single most important optimization for method handle performance.

Avoid unnecessary depth

Each combinator (filterArguments, guardWithTest, etc.) adds a layer of indirection. The JIT can often see through these layers, but only up to a point. If you find yourself chaining five or six combinators deep, consider whether a simple helper method would be clearer and more easily optimized.

Prefer stable call profiles

A method handle stored on a static final field gives the JIT a monomorphic call profile, meaning: it always sees the same handle. If a handle is loaded from a mutable field or passed as a parameter, the JIT may see different handles at different times, which can inhibit inlining. Where possible, arrange for handles to be stable constants.

Next...

The next post will introduce VarHandle: a related abstraction for fine-grained variable access with explicit control over memory ordering and atomic operations.