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

2022-08-31 06:09:08

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

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

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

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

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


答案 1

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

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

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


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

总结:

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

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

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

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

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


答案 2

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

进一步请注意,这方面取决于实现,您可以期待HotSpot的未来改进和进步,以提高效率。例如,有一些一般的计划是在没有完整对应类的情况下制作一个轻量级对象,该类具有足够的信息来转发到单个方法。

这是一篇关于该主题的优秀,可访问的深入文章:

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


推荐