Lambda Metafactory Variable Capture

2020-07-07 11:47发布

问题:

When creating a lambda manually using MethodHandles.Lookup, MethodHandles, MethodTypes, 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?

回答1:

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



回答2:

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