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

218

当我在Java 8中使用新的语法糖来迭代集合时,比如

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

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

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

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


70
简短回答:不会创建多个状态无关的 lambda 函数实例(即不从其词法上下文中获取任何内容的 lambda 函数)。仅会在捕获点进行懒加载,并缓存一个实例。这是实现的方式;规范经过精心编写,允许但不要求采用此方法。 - Brian Goetz
8
http://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood - Broken_Window
3个回答

196

它们是等效的但不完全相同。简单来说,如果一个lambda表达式不捕获值,它将成为一个单例,在每次调用时都会被重复使用。

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

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


这段文字在《Java®语言规范》的“第15.27.4节Lambda表达式的运行时评估”中有所涉及。

总结:

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

  • 不需要在每次评估时分配新对象。

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

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

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


请问您能在回答中解释一下这个句子的意思吗?"如果一个lambda表达式没有捕获值"。我有点困惑。 - H.S
2
@H.S 类似于内部类。访问周围上下文的局部变量将捕获其值,并在评估 lambda 表达式主体时使用该值。访问周围对象的实例字段需要捕获 this 引用的值,以便在评估 lambda 表达式主体时能够读取该字段的值。您可以按照答案中给出的链接(这个链接)查看实际示例。 - Holger

29
创建表示lambda的实例时,取决于lambda体的确切内容。即,关键因素是lambda从词法环境中捕获的状态。如果它不捕获任何在每次进入for-each循环时都会变化的状态,则不会每次创建实例。相反,编译时将生成一个合成方法,并且lambda使用站点将只接收一个委托给该方法的单例对象。
此外,请注意这方面是与实现相关的,您可以期望HotSpot未来对其进行改进和提高效率。有一般计划,例如制作轻量级对象而无需完整对应类,该对象只有足够的信息以转发到单个方法。
以下是有关此主题的良好、易于理解的深入文章: http://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood

0

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

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

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

是的。它在非常低的级别上有轻微的差异,但我认为您不应该关心它们。Lambda表达式使用invokedynamic功能而不是匿名类。


你有关于这个的任何文档吗? 这是一个非常有趣的优化。 - A. Rama
谢谢,但是如果我有一个集合的集合的集合怎么办?例如,在树数据结构上进行深度优先搜索时。 - Bastian Voigt
3
@A.Rama 抱歉,我没有看到优化。使用lambda表达式或者forEach循环都不会有任何区别。 - aalku
1
虽然不完全相同,但每个嵌套级别最多只需要一个对象,这是可以忽略的。每次内部循环的新迭代都会创建一个新对象,很可能捕获外部循环的当前项。这会产生一些GC压力,但仍然不必担心。 - Marko Topolnik
7
“每次你这样做,都会创建一个新对象”:根据Holger在此答案和Brian Goetz在此评论中的说法,并非如此。 - Lii
@lii 你是对的,但并非总是如此。如果lambda捕获了一个值,那么它就不是单例,这一切都取决于JVM。 - aalku

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接