以编程方式创建Java8函数引用

时间:2015-11-20 19:08:41

标签: java java-8 method-reference

只是一个理论问题,我目前没有实际用例。

假设我的API接受函数引用作为参数,我想通过'::'语法直接从代码中提取它,或者通过反射收集匹配函数,存储在某些Map中并有条件地调用。

可以通过编程方式将method转换为Consumer<String>

Map<String, Consumer<String>> consumers = new HashMap<>();
consumers.put("println", System.out::println);

Method method = PrintStream.class.getMethod("println", String.class);
consumers.put("println", makeFunctionReference(method));
...
myapi.feedInto(consumers.get(someInput.getConsumerId()));

更新

虽然目前提供的答案中的解决方案不满意,但在得到关于LambdaMetaFactory的提示之后我试图编译此代码

public class TestImpl {
    public static void FnForString(String arg) {}
}

public class Test {
    void test() {
        List<String> strings = new ArrayList<>();
        Consumer<String> stringConsumer = TestImpl::FnForString;

        strings.stream().forEach(stringConsumer);
        strings.stream().forEach(TestImpl::FnForString);
        stringConsumer.accept("test");
    }
}

并且只将Test类送入CFR反编译器后,我正在追踪:

public class Test {
    void test() {
        ArrayList strings = new ArrayList();
        Consumer<String> stringConsumer = 
            (Consumer<String>)LambdaMetafactory.metafactory(
                null, null, null, 
                (Ljava/lang/Object;)V, 
                FnForString(java.lang.String), 
                (Ljava/lang/String;)V)();
        strings.stream().forEach(stringConsumer);
        strings.stream().forEach(
            (Consumer<String>)LambdaMetafactory.metafactory(
                null, null, null, 
                (Ljava/lang/Object;)V, 
                FnForString(java.lang.String ), 
                (Ljava/lang/String;)V)());
        stringConsumer.accept("test");
    }
}

其中我看到了:

  • 以某种方式可以用'1-liner'方式进行
  • 无需处理异常
  • 我不知道反编译器输出中的(Ljava/lang/Object;)V(和其他人)是什么。它应与metafactory()参数中的MethodType匹配。另外 - 反编译器'吃掉/隐藏'某些东西,但现在似乎在获取函数引用期间调用了方法。
  • (offtop)即使在编译代码中获取函数引用也至少有一个函数调用 - 通常,这在性能关键代码中可能并不是不显着的廉价操作。

并且......提供了Test和TestImpl类,CFR重建了与我编译完全相同的代码。

3 个答案:

答案 0 :(得分:5)

你可以用这样的反射做到这一点:

consumers.put("println", s -> {
    try {
        method.invoke(System.out, s);
    } catch (InvocationTargetException | IllegalAccessException e) {
        throw new RuntimeException(e);
    }
});

但是您希望您的代码使用方法引用(即使用invokedynamic指令)编译为相同的代码,您可以使用MethodHandle。这没有反射的开销,所以它会表现得更好。

MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType methodType = MethodType.methodType(void.class, String.class);
MethodHandle handle = lookup.findVirtual(PrintStream.class, "println", methodType);

consumers.put("println", s -> {
    try {
        handle.invokeExact(System.out, s);
    } catch (Throwable e) {
        throw new RuntimeException(e);
    }
});

consumers.get("println").accept("foo");

在此代码中,首先检索MethodHandles.Lookup个对象。该类可用于创建MethodHandle个对象。然后创建一个MethodType对象,它表示由方法句柄接受和返回的参数和返回类型:在这种情况下,它是一个返回void(因此void.class)并接受一个String的方法(因此String.class)。最后,通过在println类上找到PrintStream方法获得句柄。

您可以参考this question(和this one),了解有关MethodHandle的更多信息。

答案 1 :(得分:3)

最简单但最不具备性能的方法就是将方法包装到消费者手中。

final Method m = ...
final T target = ...

Consumer<String> c = (arg1) => m.invoke(t, arg1);

使用LambdaMetaFactory可能会产生更优化的代码,但考虑到您通过Map进行调度,可能不值得。

  

这在某种程度上可以用'1-liner'方式进行

如果你真的想模仿字节码所做的事情,那对于单线程的充分折磨的定义来说这是唯一的。你的反编译器在某种程度上对你有用。

  

无需处理异常

这是因为在字节码级别上不存在已检查异常的概念。这可以使用为您执行sneaky rethrow的静态帮助程序方法进行模拟。

  

我不知道反编译器输出中的(Ljava / lang / Object;)V(和其他)是什么。它应该与metafactory()参数中的MethodType匹配。另外 - 反编译器'吃掉/隐藏'某些东西,但现在似乎在获取函数引用期间调用了方法。

它看起来像invokedynamic调用的伪代码。 JVM真正做的是更复杂,并且无法在java中简明扼要地表达,因为它涉及延迟初始化。最好阅读java.lang.invoke package description以了解真实情况。

与链接阶段等效的java级别将把CalleSite的dynamicInvoker MH放入static final MethodHandle字段并调用其invokeExact方法。

  

(offtop)即使在编译代码中获取函数引用也至少有一个函数调用 - 通常这在性能关键代码中可能并不是一个不显着的廉价操作。

如上所述,链接阶段相当于将methodhandle放在静态字段中,然后再调用它,而不是尝试再次解析该方法。

答案 2 :(得分:2)

反编译器在你的代码上失败了,但是,除了重新创建原始的Java 8方法引用之外,没有正确的反编译,这不是你感兴趣的。

lambda表达式和方法引用是使用score=[] score[rcount][ccount]=num 字节代码指令编译的,该指令在Java编程语言中没有等价物。等效代码如下:

invokedynamic

除了在public static void main(String... arg) { Consumer<String> consumer=getConsumer(); consumer.accept("hello world"); } static Consumer<String> getConsumer() { try { MethodHandles.Lookup lookup=MethodHandles.lookup(); MethodType consumeString = MethodType.methodType(void.class, String.class); return (Consumer<String>)LambdaMetafactory.metafactory(lookup, "accept", MethodType.methodType(Consumer.class, PrintStream.class), consumeString.changeParameterType(0, Object.class), lookup.findVirtual(PrintStream.class, "println", consumeString), consumeString) .getTarget().invokeExact(System.out); } catch(RuntimeException | Error e) { throw e; } catch(Throwable t) { throw new BootstrapMethodError(t); } } 内完成的所有操作最初都由一条getConsumer()指令处理,该指令会处理所有涉及的invokedynamicMethodHandle实例,例如常量和第一次评估的结果获得了内在的缓存工具。您无法使用普通的Java源代码对其进行建模。

仍然,上面的MethodType方法返回的Consumer<String>与表达式getConsumer()(当分配给System.out::println时)完全等效,具有相同的行为和性能特性

您可以学习“Translation of Lambda Expressions” by Brian Goetz以深入了解其工作原理。此外,API documentation of LambdaMetafactory非常详尽。