Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lambda Metafactory Variable Capture

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?

like image 393
Mona the Monad Avatar asked Jun 03 '15 23:06

Mona the Monad


People also ask

Can Lambda capture the local variable in Java?

Lambda expression in java Using a local variable is as stated. However, when a lambda expression uses a local variable from its enclosing scope, a special situation is created that is referred to as a variable capture. In this case, a lambda expression may only use local variables that are effectively final.

Can we declare variable in lambda Java?

A lambda expression can't define any new scope as an anonymous inner class does, so we can't declare a local variable with the same which is already declared in the enclosing scope of a lambda expression. Inside lambda expression, we can't assign any value to some local variable declared outside the lambda expression.

What is LambdaMetafactory?

java.lang.Object. java.lang.invoke.LambdaMetafactory. public final class LambdaMetafactory extends Object. Methods to facilitate the creation of simple "function objects" that implement one or more interfaces by delegation to a provided MethodHandle , possibly after type adaptation and partial evaluation of arguments.

How lambda expression works in Java?

A lambda expression is a short block of code which takes in parameters and returns a value. Lambda expressions are similar to methods, but they do not need a name and they can be implemented right in the body of a method.


2 Answers

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

like image 60
Holger Avatar answered Oct 04 '22 21:10

Holger


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);
}
like image 39
Misha Avatar answered Oct 04 '22 23:10

Misha