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


182

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

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

这不等同于下面的“旧语法”代码段吗?

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

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


48
简短的回答:不。对于无状态Lambda(那些无法从其词法上下文中捕获任何内容的Lambda),将仅(延迟)创建一个实例并将其缓存在捕获站点中。(这是实现的工作方式;该规范经过精心编写以允许但不要求这种方法。)
Brian Goetz 2014年

Answers:


158

它是等效的但不相同。简而言之,如果lambda表达式未捕获值,则它将是单例,可在每次调用时重复使用。

行为没有完全指定。JVM在实现方面具有很大的自由度。当前,Oracle的JVM为每个lambda表达式创建(至少)一个实例(即不在不同的相同表达式之间共享实例),但为所有不捕获值的表达式创建单例。

您可以阅读此答案以获取更多详细信息。在那里,我不仅给出了更详细的描述,还提供了测试代码以观察当前行为。


Java®语言规范的第15.27.4对此进行了介绍。Lambda表达式的运行时评估

总结:

这些规则旨在通过以下方式为Java编程语言的实现提供灵活性:

  • 不必在每次评估中分配一个新对象。

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

  • 评估产生的每个对象不必属于同一类(例如,可以内联捕获的局部变量)。

  • 如果“现有实例”可用,则不必在先前的lambda评估中创建它(例如,可能在封闭类的初始化期间分配了它)。


24

创建代表lambda的实例时,敏感地取决于lambda身体的确切内容。即,关键因素是lambda 从词法环境中捕获的内容。如果它没有捕获创建到创建之间可变的任何状态,则不会在每次进入for-each循环时都创建一个实例。相反,将在编译时生成综合方法,并且lambda使用站点将只接收委托给该方法的单例对象。

还要注意,这方面与实现有关,您可以期望HotSpot将来会进行改进和提高效率。有一般的计划,例如,制作没有完整对应类的轻量级对象,该类仅具有足够的信息以转发给单个方法。

这是一篇很好的,可访问的有关该主题的深入文章:

http://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood


0

您正在将新实例传递给该forEach方法。每次执行此操作时,都会创建一个新对象,但不会为每次循环迭代创建一个对象。迭代是forEach使用相同的“回调”对象实例在方法内部完成的,直到通过循环完成为止。

因此,循环使用的内存不取决于集合的大小。

这不等同于“旧语法”代码段吗?

是。它在非常低的水平上有细微的差异,但我认为您不应该在意它们。Lamba表达式使用invokedynamic功能而不是匿名类。


您是否有任何文件说明这一点?这是非常有趣的优化。
拉玛

谢谢,但是如果我有一个集合的集合,例如在对树数据结构进行深度优先搜索时,该怎么办?
巴斯蒂安·福伊特

2
@ A.Rama对不起,我没有看到优化。无论有无lambda以及有无forEach循环,它都是相同的。
aalku

1
并不是完全一样,但是在任何时候,每个嵌套级别最多仍需要一个对象,这可以忽略不计。内循环的每个新迭代都会创建一个新对象,最有可能从外循环捕获当前项目。这会产生一些GC压力,但仍然没有真正要担心的问题。
Marko Topolnik,2014年

4
@aalku:“ 每次执行此操作时,您都会创建一个新对象 ”:不符合Holger的回答Brian Goetz的评论
Lii
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.