When creating a lambda manually using MethodHandles.Lookup
, MethodHandle
s, MethodType
s, etc, how might one implement variable capture?
For example, with no capture:
public IntSupplier foo() {
return this::fortyTwo;
}
/**
* Would not normally be virtual, but oh well.
*/
public int fortyTwo() {
return 42;
}
and its clunkier form, using stuff in java.lang.invoke
:
public IntSupplier foo() {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType methodType = MethodType.methodType(int.class),
lambdaType = MethodType.methodType(IntSupplier.class);
MethodHandle methodHandle = lookup.findVirtual(getClass(), "fortyTwo", methodType);
CallSite callSite = LambdaMetafactory.metafactory(lookup, "getAsInt", lambdaType, methodType, methodHandle, methodType);
return (IntSupplier) callSite.getTarget().invokeExact();
}
/**
* Would not normally be virtual, but oh well.
*/
public int fortyTwo() {
return 42;
}
would return a simple, pointless IntSupplier
that returns 42
when invoked, but what if one would like to capture something?
The third argument to the bootstrap method, which you named lambdaType
, is the invoked type of the associated invokedynamic
instruction (normally filled in by the JVM). It’s semantic is defined by the bootstrap method and in the case of the LambdaMetaFactory
, it specifies the functional interface as return type (the type of the object to construct) and the values to capture as parameter type (the type of the values to consume when constructing a lambda instance).
So in order to capture this
, you have to add the type of this
to your invoked type and pass this
as an argument to the invokeExact
call:
public class Test {
public static void main(String... arg) throws Throwable {
System.out.println(new Test().foo().getAsInt());
}
public IntSupplier foo() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType methodType = MethodType.methodType(int.class),
invokedType = MethodType.methodType(IntSupplier.class, Test.class);
MethodHandle methodHandle = lookup.findVirtual(getClass(), "fortyTwo", methodType);
CallSite callSite = LambdaMetafactory.metafactory(lookup, "getAsInt",
invokedType, methodType, methodHandle, methodType);
return (IntSupplier) callSite.getTarget().invokeExact(this);
}
public int fortyTwo() {
return 42;
}
}
If you want to capture more values, you have to add them to the signature in the right order. E.g., to capture another int
value:
public class Test {
public static void main(String... arg) throws Throwable {
System.out.println(new Test().foo(100).getAsInt());
}
public IntSupplier foo(int capture) throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType methodType = MethodType.methodType(int.class, int.class),
functionType = MethodType.methodType(int.class),
invokedType = MethodType.methodType(IntSupplier.class, Test.class, int.class);
MethodHandle methodHandle=lookup.findVirtual(getClass(),"addFortyTwo",methodType);
CallSite callSite = LambdaMetafactory.metafactory(lookup, "getAsInt",
invokedType, functionType, methodHandle, functionType);
return (IntSupplier) callSite.getTarget().invokeExact(this, capture);
}
public int addFortyTwo(int valueToAdd) {
return 42+valueToAdd;
}
}
The target method will have a signature consisting of the this
type, if not static
, followed by all parameter types. The capture values will map in order to this signature’s types from left to right and the remaining parameter types, if any, contribute to the functional signature, hence have to match the interface
method’s parameter types.
This implies that when there are no captured values and the target method is not static
, the method receiver type might become associated with the first type of the functional signature, as in ToIntFunction<String> f=String::length;
.
Your code will not run. Since your fortyTwo
method is not static, you would have to capture this
by using MethodType.methodType(IntSupplier.class, getClass())
as the 3rd argument to metafactory
and then passing this
as an argument to invokeExact
.
Here's an example of capturing a string using a static method to keep things simpler:
public static int len(String s) {
return s.length();
}
public IntSupplier supplyLength(String capture) throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
CallSite callSite = LambdaMetafactory.metafactory(
lookup,
"getAsInt",
methodType(IntSupplier.class, String.class),
methodType(int.class),
lookup.findStatic(getClass(), "len", methodType(int.class, String.class)),
methodType(int.class)
);
return (IntSupplier) callSite.getTarget().invoke(capture);
}