Explicit use of LambdaMetafactory
Asked Answered
W

4

29

I'm trying to explicitly use LambdaMetafactory.metafactory, I can't understand why it works only with the Runnable functional interface. For Example, this code does what it is expected (it prints "hello world"):

public class MetafactoryTest {
    
    public static void main(String[] args) throws Throwable {

        MethodHandles.Lookup caller = MethodHandles.lookup();
        MethodType methodType = MethodType.methodType(void.class);
        MethodType invokedType = MethodType.methodType(Runnable.class);
        CallSite site = LambdaMetafactory.metafactory(caller, 
                                                      "run", 
                                                      invokedType, 
                                                      methodType, 
                                                      caller.findStatic(MetafactoryTest.class, "print", methodType), 
                                                      methodType);
        MethodHandle factory = site.getTarget();
        Runnable r = (Runnable) factory.invoke();
        r.run();
    }
    
    private static void print() {
        System.out.println("hello world"); 
    }    
}

The problem arises when I try to use a different functional interface, such as Supplier. The following code does not work:

public class MetafactoryTest {

    public static void main(String[] args) throws Throwable {

        MethodHandles.Lookup caller = MethodHandles.lookup();
        MethodType methodType = MethodType.methodType(String.class);
        MethodType invokedType = MethodType.methodType(Supplier.class);
        CallSite site = LambdaMetafactory.metafactory(caller, 
                                                      "get", 
                                                      invokedType, 
                                                      methodType, 
                                                      caller.findStatic(MetafactoryTest.class, "print", methodType), 
                                                      methodType);
        MethodHandle factory = site.getTarget();
        Supplier<String> r = (Supplier<String>) factory.invoke();
        System.out.println(r.get());        
    }
    private static String print() {
        return "hello world";
    }    
}


Exception in thread "main" java.lang.AbstractMethodError: metafactorytest.MetafactoryTest$$Lambda$1/258952499.get()Ljava/lang/Object;
    at metafactorytest.MetafactoryTest.main(MetafactoryTest.java:29)

Shouldn't the two snippets of code work in a similar way, what is the problem in the second one?

Moreover, the following code which should be equivalent, works well:

public class MetafactoryTest {

    public static void main(String[] args) throws Throwable {
        Supplier<String> r = (Supplier<String>) () -> print();
        System.out.println(r.get());        
    }

    private static String print() {
        return "hello world";
    }    
}

Edit

Another solution that avoids changing the method return type is to define a new functional interface:

public class MetafactoryTest {

    @FunctionalInterface
    public interface Test {
        String getString();
    }
    
    public static void main(String[] args) throws Throwable {

        MethodHandles.Lookup caller = MethodHandles.lookup();
        MethodType methodType = MethodType.methodType(String.class);
        MethodType invokedType = MethodType.methodType(Test.class);
        CallSite site = LambdaMetafactory.metafactory(caller, 
                                                      "getString", 
                                                      invokedType, 
                                                      methodType, 
                                                      caller.findStatic(MetafactoryTest.class, "print", methodType), 
                                                      methodType);
        MethodHandle factory = site.getTarget();
        Test r = (Test) factory.invoke();
        System.out.println(r.getString());        
    }
    
    private static String print() {
        return "hello world";
    }  
Walli answered 6/11, 2014 at 9:20 Comment(2)
Perhaps the problem is with the method name "run" you pass as the second argument. Runnable has a "run" method. Supplier does not.Cocaine
That was an error (The Runnable case works only with "run"), but also with get the second snippet gives that error.Walli
E
25

The difference between Runnable and Supplier is that Supplier uses a generic type.

At runtime Supplier doesn't have a String get() method, it has Object get(). But the method you implement returns a String. You need to distinguish between those 2 types. Like this:

public class MetafactoryTest {

    public static void main(String[] args) throws Throwable {

        MethodHandles.Lookup caller = MethodHandles.lookup();
        MethodType methodType = MethodType.methodType(Object.class);
        MethodType actualMethodType = MethodType.methodType(String.class);
        MethodType invokedType = MethodType.methodType(Supplier.class);
        CallSite site = LambdaMetafactory.metafactory(caller, 
                                                      "get", 
                                                      invokedType, 
                                                      methodType, 
                                                      caller.findStatic(MetafactoryTest.class, "print", actualMethodType), 
                                                      methodType);
        MethodHandle factory = site.getTarget();
        Supplier<String> r = (Supplier<String>) factory.invoke();
        System.out.println(r.get());
    }

    private static String print() {
        return "hello world";
    }    
}
Encephalo answered 6/11, 2014 at 9:40 Comment(4)
Would this work if the print method contained an argument?Rabin
If the print method needs an argument it cannot be used to implement the "Runnable" or the "Supplier" interface.Encephalo
Is their anything that can be used? Which would you recommend? In my instance, I'm trying to invoke a non-static function that returns a boolean and takes in one or more Strings as parameters.Rabin
@Sahil Gupta: you can bind values as shown in this answer. The first bound value will be the method receiver, if the target method is not static, other bound values map to the method’s parameters left to right. The function parameters, if any, map to the remaining unbound method parameters.Undertrick
H
4

Late to the game but LambdaMetaFactory almost drove me crazy. Very hard to see which parameter goes where. Made an example that shows the different types a bit more explicit by naming them according to their role.

    class Instance {
        public RetSub get(Par p) {
            System.out.println("Yes");
            return null;
        }
    }
    static class Ret {}
    static class RetSub extends Ret {}
    static class Par {}
    static class ParSub extends Par {}
    interface If {
        Ret method(ParSub p);
    }

    @Test
    public void testInstance() throws Throwable {
        Instance instance = new Instance();
        CallSite site = LambdaMetafactory.metafactory(caller,
            "method",
            MethodType.methodType(If.class, Instance.class), //
            MethodType.methodType(Ret.class, ParSub.class), //
            caller.findVirtual(Instance.class, "get", MethodType.methodType(RetSub.class, Par.class)), //
            MethodType.methodType(RetSub.class, ParSub.class));

        MethodHandle factory = site.getTarget();

        If iff = (If) factory.invoke(instance);
        iff.method(new ParSub());
    }

I think it was hard because I missed the semantics of the parameters.

  • invokedName – Obviously the method name in the interface
  • invokedType – This is the hard one. This is the MethodType of the MethodHandle returned by the CallSite.getTarget(). The first argument is the type of the interface we're trying to make (If), the second argument is only needed when we access an instance method and is the instance type (Instance).
  • samMethodType – This is the exact method type of the function we try to implement. In this case 'Ret method(ParSub p)`.
  • implMethod – A method handle. It must be possible to convert the parameters and return type of the interface method we create to this method handle.
  • instantiationMethodType – A runtime check to verify some parameters and return types fall within narrower or the same types than the interface.
Holdback answered 6/9, 2021 at 17:38 Comment(0)
R
2

This is another example with a more easy to understand variable names:

public class Demo {
    public static void main(String[] args) throws Throwable {
        Consumer<String> consumer = s -> System.out.println("CONSUMED: " + s + ".");

        consumer.accept("foo");

        MethodHandles.Lookup caller = MethodHandles.lookup();

        MethodType lambdaBodyMethodType = MethodType.methodType(void.class, String.class);
        MethodHandle lambdaBody = caller.findStatic(
                Demo.class, "my$lambda$main$0", lambdaBodyMethodType);

        // Because of the type erasure we must use Object here
        // instead of String (Consumer<String> -> Consumer).
        MethodType functionalInterfaceMethodType =
                MethodType.methodType(void.class, Object.class);

        // we must return consumer, no closure -> no additional parameters
        MethodType callSiteType = MethodType.methodType(Consumer.class);

        CallSite site = LambdaMetafactory.metafactory(
                // provided by invokedynamic:
                caller, "accept", callSiteType,
                // additional bootstrap method arguments:
                functionalInterfaceMethodType,
                lambdaBody,
                lambdaBodyMethodType);

        MethodHandle factory = site.getTarget();
        Consumer<String> r = (Consumer<String>) factory.invoke();

        r.accept("foo");
        r.accept("bar");
    }

    private static void my$lambda$main$0(String s) {
        System.out.println("CONSUMED: " + s + ".");
    }
}

Because LambdaMetafactory creates a synthetic factory class that then is used to create target interface, callSiteType has a type of this factory create() method. This create() method is called implicitly by invokedynamic - LambdaMetafactory returns a CallSite that has a method reference to the create method. For lambdas with closures you will call the factory like factory.create(capturedValue1, ..., capturedValueN) and so you must modify callSiteType accordingly.

Rapids answered 22/10, 2017 at 13:27 Comment(0)
I
2

I had a situation where I needed to call a function passing some parameter to it. Similar to @Sahil Gupta question. I managed to solve it using a BiFunction with some adjustments:

public void testFunctionWithParameter() throws Throwable {
    SimpleBean simpleBean = new SimpleBean();

    MethodHandles.Lookup caller = MethodHandles.lookup();
    MethodType invokedType = MethodType.methodType(BiFunction.class);
    MethodType biFunc = MethodType.methodType(String.class, String.class);
    MethodHandle target = caller.findVirtual(SimpleBean.class, "simpleFunction", biFunc);
    MethodType func = target.type();


    CallSite site = LambdaMetafactory.metafactory(
            caller,
            "apply",
            invokedType,
            func.generic(),
            target,
            MethodType.methodType(String.class, SimpleBean.class, String.class)
    );

    BiFunction<SimpleBean, String, String> fullFunction = (BiFunction<SimpleBean, String, String>) site.getTarget().invokeExact();


    System.out.println(fullFunction.apply(simpleBean, "FOO"));

}

private class SimpleBean {
    public String simpleFunction(String in) {
        return "The parameter was " + in;
    }
}

I hope it helps someone.

Insuppressible answered 28/7, 2019 at 19:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.