首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >以编程方式创建Java8函数引用

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

Stack Overflow用户
提问于 2015-11-20 19:08:40
回答 3查看 1.1K关注 0票数 6

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

假设我的一些API接受函数引用作为参数,我希望通过“::”语法直接从代码中输入函数,或者通过反射收集匹配函数,将其存储在某个Map中,并有条件地调用。

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

代码语言:javascript
复制
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的提示之后,我尝试编译这段代码

代码语言:javascript
复制
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");
    }
}

在只向CFR反编译器中提供测试类之后,我将得到以下结果:

代码语言:javascript
复制
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 (和其他)是什么。它应该在MethodType参数中与元角()匹配。另外-反编译器‘吃/隐藏’的东西,但现在似乎有调用的方法在获取函数引用。
  • (offtop)即使在编译的代码中获得函数引用也至少是一个函数调用--一般来说,在性能关键代码中,这可能不是不明显的廉价操作。

还有..。提供了测试类和TestImpl类之后,CFR就可以重新构造我编译的代码。

EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2015-11-20 19:47:02

你可以用这样的反射来做这个:

代码语言:javascript
复制
consumers.put("println", s -> {
    try {
        method.invoke(System.out, s);
    } catch (InvocationTargetException | IllegalAccessException e) {
        throw new RuntimeException(e);
    }
});

但是,如果希望使用方法引用(即使用invokedynamic指令)将代码编译成相同的代码,则可以使用MethodHandle。这没有反射的开销,因此它的性能会好得多。

代码语言:javascript
复制
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.class)的方法。最后,通过在println类上找到PrintStream方法获得句柄。

您可以参考这个问题 (和这一个)获得关于MethodHandles的更多信息。

票数 5
EN

Stack Overflow用户

发布于 2015-11-20 19:48:42

最简单的方法,尽管不一定是最有表现力的方法,就是将方法包装到消费者中。

代码语言:javascript
复制
final Method m = ...
final T target = ...

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

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

这在某种程度上可以用“1-liner”的方式来实现。

如果您真的想模仿字节码所做的事情,那么这只适用于对一行行的足够严苛的定义。你的反编译者在某种程度上对你撒谎。

不需要异常处理。

这是因为字节码级别上不存在检查异常的概念。这可以通过为您执行鬼鬼祟祟地重投的静态助手方法来模拟。

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

它看起来像是发票动态调用的伪代码。JVM真正做的事情更复杂,不能用java简洁地表达,因为它涉及延迟初始化。最好读一读java.lang.invoke包描述来了解到底发生了什么。

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

(offtop)即使在编译的代码中获得函数引用也至少是一个函数调用--一般来说,在性能关键代码中,这可能不是不明显的廉价操作。

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

票数 3
EN

Stack Overflow用户

发布于 2015-11-24 18:10:01

反编译程序在您的代码上失败了,但是,除了重新创建原来的Java 8方法引用(这不是您感兴趣的内容)之外,无论如何也没有正确的反编译。

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

代码语言:javascript
复制
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源代码对其进行建模。

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

您可以学习Brian的“Lambda表达式的翻译”,以便更深入地了解它的工作原理。此外,LambdaMetafactory也相当详尽。

票数 3
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/33834288

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档