lambda表达式每次执行时在堆上创建一个对象吗?

当我使用Java 8的新语法糖遍历一个集合时,例如

myStream.forEach(item -> {
// do something useful
});

这难道不等同于下面的“旧语法”片段吗?

myStream.forEach(new Consumer<Item>() {
@Override
public void accept(Item item) {
// do something useful
}
});

这是否意味着每次遍历一个集合时,都会在堆上创建一个新的匿名Consumer对象?这需要多少堆空间?它有什么性能影响?这是否意味着在迭代大型多级数据结构时,我应该使用旧风格的for循环?

41923 次浏览

您正在将一个新实例传递给forEach方法。每次这样做时,都会创建一个新对象,但不是每次循环迭代都创建一个新对象。迭代是在forEach方法中使用相同的“回调”对象实例完成的,直到循环完成。

因此,循环所使用的内存不依赖于集合的大小。

这难道不等同于“旧语法”片段吗?

是的。它在非常低的水平上有轻微的差异,但我认为你不应该关心它们。Lamba表达式使用invokedynamic特性而不是匿名类。

它们是等价的,但不完全相同。简单地说,如果lambda表达式不捕获值,那么它将是在每次调用中重用的单例。

行为没有被精确地指定。JVM在如何实现它方面有很大的自由。目前,Oracle的JVM为每个lambda表达式创建(至少)一个实例(即不会在不同相同的表达式之间共享实例),但为所有不捕获值的表达式创建单例。

你可以阅读这个答案了解更多细节。在那里,我不仅给出了更详细的描述,而且还测试了代码来观察当前的行为。


这在Java®语言规范“15.27.4. Lambda表达式的运行时计算”章节中有介绍

总结:

这些规则旨在为Java编程语言的实现提供灵活性,因为:

  • 不需要在每次求值时分配一个新对象。

  • 由不同lambda表达式生成的对象不需要属于不同的类(例如,如果主体相同)。

  • 由求值生成的每个对象不需要属于同一个类(例如,捕获的局部变量可能是内联的)。

  • 如果一个"现有实例"可用时,它不需要在以前的lambda求值时创建(例如,它可以在外围类初始化期间分配)。

何时敏感地创建表示lambda的实例取决于lambda主体的确切内容。也就是说,关键因素是lambda 捕捉来自词汇环境。如果它没有捕获任何创建到创建之间变化的状态,那么每次进入for-each循环时都不会创建实例。相反,在编译时将生成一个合成方法,lambda use站点将只接收一个委托给该方法的单例对象。

进一步注意,这方面是依赖于实现的,您可以期待HotSpot未来的改进和进步,以提高效率。有一些一般的计划,例如,创建一个轻量级的对象,没有一个完整的对应类,它有足够的信息转发给单个方法。

下面是一篇关于这个主题的好文章:

http://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood < a href = " http://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood " > < / >