Подтвердить что ты не робот

Явное использование LambdaMetafactory

Я пытаюсь явно использовать LambdaMetafactory.metafactory, я не понимаю, почему он работает только с функциональным интерфейсом Runnable. Например, этот код делает то, что ожидается (он печатает "привет мир" ):

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

Проблема возникает, когда я пытаюсь использовать другой функциональный интерфейс, такой как Поставщик. Следующий код не работает:

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)

Не должны ли два фрагмента кода работать аналогичным образом, что является проблемой во втором фрагменте кода?

Кроме того, следующий код, который должен быть эквивалентен, хорошо работает:

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

Другим решением, которое позволяет избежать изменения типа возвращаемого метода, является определение нового функционального интерфейса:

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";
    }  
4b9b3361

Ответ 1

Разница между Runnable и поставщиком заключается в том, что поставщик использует общий тип.

В среде выполнения поставщик не имеет метода String get(), он имеет Object get(). Но метод, который вы реализуете, возвращает String. Вам нужно различать эти 2 типа. Вот так:

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

Ответ 2

Это еще один пример с более понятными именами переменных:

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 + ".");
    }
}

Потому что LambdaMetafactory создает синтетический класс factory, который затем используется для создания целевого интерфейса, callSiteType имеет тип этого метода factory create(). Этот метод create() неявно называется invokedynamic - LambdaMetafactory возвращает CallSite, который имеет ссылку на метод метода create. Для лямбда с закрытием вы вызываете factory как factory.create(capturedValue1, ..., capturedValueN), поэтому вы должны соответствующим образом изменить callSiteType.

Ответ 3

У меня была ситуация, когда мне нужно было вызвать функцию, передав ей какой-то параметр. Похоже на вопрос @Sahil Gupta. Мне удалось решить эту проблему с помощью BiFunction с некоторыми изменениями:

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

Надеюсь, это кому-нибудь поможет.