在Java 8中使用方法引用语法而不是lambda语法是否会对性能产生好处?


56

方法引用是否跳过了lambda包装器的开销?他们将来可能吗?

根据有关方法参考Java教程

有时... lambda表达式除了调用现有方法外什么也不做。在这种情况下,通常更容易按名称引用现有方法。方法引用使您可以执行此操作;它们是紧凑的,易于阅读的lambda表达式,用于已经具有名称的方法。

由于以下几个原因,我更喜欢lambda语法而不是方法引用语法:

Lambda更清晰

尽管有Oracle的主张,但我发现lambda语法比对象方法引用的简写形式更易于阅读,因为方法引用的语法是模棱两可的:

Bar::foo

您是否在x的类上调用静态的一参数方法并将其传递给x?

x -> Bar.foo(x)

还是在x上调用零参数实例方法?

x -> x.foo()

方法参考语法可以代表任何一种。它隐藏了您的代码实际在做什么。

Lambda更安全

如果将Bar :: foo引用为类方法,并且Bar以后添加了同名的实例方法(反之亦然),则代码将不再编译。

您可以始终使用lambda

您可以将任何函数包装在lambda中-因此您可以在所有位置一致地使用相同的语法。方法引用语法不适用于采用或返回原始数组,引发已检查异常或具有与实例和静态方法相同的方法名称的方法(因为该方法引用语法对于调用哪种方法尚不明确) 。当您使用相同数量的参数重载方法时,它们不起作用,但是无论如何您都不应这样做(请参见Josh Bloch的第41项),因此我们不能将其与方法引用相对应。

结论

如果这样做不影响性能,我很想在我的IDE中关闭警告,并始终使用lambda语法,而不会将偶然的方法引用散布到我的代码中。

聚苯乙烯

无论在这里还是那里,但在我的梦中,对象方法引用看起来都更像这样,并且无需lambda包装器就直接在对象上对方法应用invoke-dynamic:

_.foo()

2
这不能回答您的问题,但是使用闭包还有其他原因。在我看来,它们中的许多远远超过了附加函数调用的微小开销。

9
我认为您过早担心性能。我认为性能应该是使用方法参考的次要考虑因素。这全是表达您的意图。如果您需要在某个地方传递一个函数,为什么不直接传递该函数而不是传递给它的其他函数呢?对我来说似乎很奇怪;就像您不太了解功能是一流的东西一样,它们需要一个匿名的伴侣来移动它们。但是为了更直接地解决您的问题,如果方法引用可以实现为静态调用,我不会感到惊讶。
Doval 2015年

7
.map(_.acceptValue()):如果我没记错的话,这看起来非常像Scala语法。也许您只是在尝试使用错误的语言。
Giorgio

2
“方法参考语法在返回void的方法上不起作用” -怎么办?我传递System.out::printlnforEach()所有的时间...?
卢卡斯·埃德

3
“方法引用语法在采用或返回原始数组,引发已检查异常的方法上将不起作用。”这是不正确的。您可以在这种情况下使用方法引用以及lambda表达式,只要相关的功能接口允许它即可。
斯图尔特·马克

Answers:


12

在许多情况下,我认为lambda和method-reference是等效的。但是,lambda将通过声明的接口类型包装调用目标。

例如

public class InvokeTest {

    private static void invoke(final Runnable r) {
        r.run();
    }

    private static void target() {
        new Exception().printStackTrace();
    }

    @Test
    public void lambda() throws Exception {
        invoke(() -> target());
    }

    @Test
    public void methodReference() throws Exception {
        invoke(InvokeTest::target);
    }
}

您将看到控制台输出stacktrace。

在中lambda(),方法调用target()lambda$lambda$0(InvokeTest.java:20),具有可追溯的行信息。显然,这就是您编写的lambda,编译器会为您生成一个匿名方法。然后,lambda方法的调用者类似于JVM中InvokeTest$$Lambda$2/1617791695.run(Unknown Source)invokedynamic调用,这意味着该调用已链接到生成的方法

在中methodReference(),方法调用target()直接是InvokeTest$$Lambda$1/758529971.run(Unknown Source),这意味着调用直接链接到该InvokeTest::target方法

结论

最重要的是,与方法引用相比,使用lambda表达式只会导致 对lambda的生成方法再进行一次方法调用。


1
下面关于元工厂的答案要好一些。我加了一些有用的意见是相关的它(即像Oracle / OpenJDK的使用ASM实现如何产生创建的λ/法处理实例所需要的包装类对象。
阿贾克斯

29

一切都与元工厂有关

首先,大多数方法引用不需要通过lambda元工厂来简化,它们只是用作引用方法。在Lambda表达式翻译(“ TLE”)文章的“ Lambda身体糖化”部分下:

在所有条件都相同的情况下,私有方法胜于非私有方法,静态方法胜于实例方法,最好是将lambda主体分解为出现lambda表达式的最内层类,签名应与lambda的主体签名相匹配,否则对于捕获的值,参数应该放在参数列表的最前面,并且根本不对方法引用进行糖化处理。但是,在某些例外情况下,我们可能不得不偏离此基准策略。

在TLE的“ Lambda Metafactory”中进一步强调了这一点:

metaFactory(MethodHandles.Lookup caller, // provided by VM
            String invokedName,          // provided by VM
            MethodType invokedType,      // provided by VM
            MethodHandle descriptor,     // lambda descriptor
            MethodHandle impl)           // lambda body

impl参数标识lambda方法,可以是经过减重的lambda主体,也可以是方法参考中命名的方法。

静态(Integer::sum)或无限制实例方法(Integer::intValue)引用是“最简单”或最“方便”的引用,从某种意义上说,可以通过“快速路径”元工厂变量 对它们进行最佳处理而无需花费很多精力。TLE的“ Metafactory变体”中指出了这一优势:

通过消除不需要的参数,类文件变得更小。快速路径选项降低了VM进行lambda转换操作的门槛,从而使其可以被视为“装箱”操作并简化了拆箱优化。

自然地,实例捕获方法参考(obj::myMethod)需要提供有界实例作为调用方法句柄的参数,这可能意味着需要使用'bridge'方法进行讨价还价。

结论

我不确定您要暗示的lambda“包装器”是什么,但是即使使用用户定义的lambda或方法引用的最终结果是相同的,但达到的方式似乎也大不相同,并且如果不是现在,将来可能会有所不同。因此,我认为元工厂更有可能以更好的方式处理方法引用。


1
这是最相关的答案,因为它实际上涉及创建lambda所需的内部机制。作为真正调试过lambda创建过程的人(为了弄清楚如何为给定的lambda /方法引用生成稳定的id,以便可以将它们从地图中删除),我感到非常惊讶的是,创建一个Lambda的实例。Oracle / OpenJDK使用ASM动态生成类,这些类在必要时关闭lambda(或方法引用的实例限定符)中引用的任何变量...
Ajax

1
...除了关闭变量之外,lambda尤其还会创建一个静态方法,该方法将被注入到声明类中,因此创建的lambda可以调用一些内容以映射到您要调用的函数中。具有实例限定符的方法引用将把该实例作为此方法的参数。我尚未测试私有类中的this :: method引用是否跳过了此关闭过程,但我测试了静态方法实际上跳过了中间方法(并只是创建了一个对象来符合调用目标)。呼叫网站)。
阿贾克斯

1
大概私有方法也不需要虚拟调度,而任何更公共的方法都需要关闭实例(通过生成的静态方法),以便它可以在实际实例上调用虚拟方法以确保它注意到重写。TL; DR:方法引用关闭了较少的参数,因此它们的泄漏较少,并且需要较少的动态代码生成。
阿贾克斯

3
最后一个垃圾评论...动态类生成功能非常强大。可能比在类加载器中加载匿名类要好(因为最重的部分只完成一次),但是您真的会惊讶于创建lambda实例需要花费多少工作。看到lambda,方法引用和匿名类的真实基准将很有趣。请注意,两个lambda或方法引用引用相同的内容,但是在不同的位置在运行时创建不同的类(即使在同一方法中,彼此紧接)。
阿贾克斯


0

使用可能影响性能的lambda表达式有一个非常严重的后果。

声明lambda表达式时,将在本地范围内创建一个闭包

这意味着什么?它对性能有何影响?

好吧,我很高兴你问。这意味着每个小lambda表达式都是一个小的匿名内部类,这意味着它带有对lambda表达式相同范围内的所有变量的引用。

这也意味着this引用对象实例及其所有字段。取决于lambda实际调用的时间,这可能构成相当大的资源泄漏,因为只要保存lambda的对象仍然存在,垃圾收集器就无法释放这些引用。


非静态方法引用也是如此。
阿贾克斯

12
Java Lambda不是严格的闭包。它们也不是匿名内部类。它们不会在声明它们的范围内携带对所有变量的引用。它们仅对实际引用的变量进行引用。这也适用于this哪些对象可以被引用。因此,lambda不会仅仅因为拥有作用域而泄漏资源。它仅保留所需的对象。另一方面,匿名内部类可以泄漏资源,但并不总是这样。参见以下示例代码:a.blmq.us/2mmrL6v
squid314

这个答案是完全错误的
Stefan Reich

谢谢@StefanReich,那非常有帮助!
罗兰·特普
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.