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

Create Java8 function reference programmatically

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

假设我的一些 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");
    }
}

并且在仅将测试 class 提供给 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");
    }
}

从中我看到:

并且...在提供了 Test 和 TestImpl classes 的情况下,CFR 重构了与我编译的完全相同的代码。

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

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 object is retrieved. This class is reponsible for creating MethodHandle objects. Then a MethodType 对象,它表示接受的参数和 return 类型并由方法句柄 return 编辑:在本例中,它是return 无效(因此 void.class)并采用字符串(因此 String.class)的方法。最后在PrintStreamclass.

上找到println方法得到句柄

您可以参考 this question (and this one) 了解更多关于 MethodHandle 是什么的信息。

最简单但不一定最高效的方法就是将方法包装到消费者中。

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

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

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


This is somehow possible to do in '1-liner' manner

如果您真的想模拟字节码的作用,那仅适用于单行代码的充分折磨定义。你的反编译器在某种程度上欺骗了你。

No exception handling is required

那是因为字节码层面不存在checked exceptions的概念。这可以通过为您执行 sneaky rethrow 的静态辅助方法来模拟。

I have no idea what is (Ljava/lang/Object;)V (and others) in decompiler's output. It should match to MethodType in metafactory() arguments. Additionally - either decompiler 'eats/hides' something, but there seems to be now invocations of methods during getting of function reference.

它看起来像是 invokedynamic 调用的伪代码。 JVM真正做的事情比较复杂,不能用java来简明表达,因为它涉及惰性初始化。最好阅读 java.lang.invoke package description 以了解实际情况。

相当于链接阶段的 java 级将把 CalleSite 的 dynamicInvoker MH 放入 static final MethodHandle 字段并调用其 invokeExact 方法。

(offtop) Obtaining function reference even in compiled code is at least one function call - in general this may be not unnoticeably cheap operation in performance critical code.

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

反编译器在您的代码上严重失败,但是,除了重新创建原始的 Java8 方法参考之外,无论如何都没有正确的反编译,这不是您感兴趣的内容。

lambda 表达式和方法引用是使用 invokedynamic 字节码指令编译的,该指令在 Java 编程语言中没有等效项。等效代码类似于:

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() 中完成的所有事情最初都是由单个 invokedynamic 指令处理的,它将所有涉及的 MethodHandleMethodType 实例视为常量并且其第一次评估的结果获得了内在的缓存设施。您无法使用普通 Java 源代码对其进行建模。

不过,上述 getConsumer() 方法返回的 Consumer<String> 与具有相同行为的表达式 System.out::println(当分配给 Consumer<String> 时)完全等价和性能特征。

你可以研究一下“Translation of Lambda Expressions” by Brian Goetz for getting a deeper understanding of how it works. Also, the API documentation of LambdaMetafactory很详尽