如何在 lambda 迭代和普通循环之间做出决定?

How to decide between lambda iteration and normal loop?

自从他介绍 Java 8 后,我就真的迷上了 lambda 并开始尽可能地使用它们,主要是为了开始习惯它们。最常见的用法之一是当我们想要迭代对象集合并对其进行操作时,在这种情况下,我会使用 forEachstream()。我很少写旧的 for(T t : Ts) 循环,我几乎忘记了 for(int i = 0.....)

但是,前几天我们和我的主管讨论了这个问题,他告诉我 lambda 并不总是最好的选择,有时会影响性能。从我看到的关于这个新功能的讲座中,我感觉 lambda 迭代总是由编译器完全优化并且会(总是?)比裸迭代更好,但他不敢苟同。这是真的?如果是,我如何区分每种情况下的最佳解决方案?

P.S:我不是说的是推荐申请parallelStream的情况。显然那些会更快。

具体实现要看

一般来说,forEach 方法和 foreach 循环 Iterator 通常具有非常相似的性能,因为它们使用相似的抽象级别。 stream() 通常较慢(通常慢 50-70%),因为它添加了另一个级别来提供对基础集合的访问。

stream() 的优点通常是可能的并行性和易于链接的操作以及 JDK 提供的大量可重用操作。

性能取决于很多因素,很难预测。通常,我们会说,如果您的主管声称性能存在问题,您的主管负责解释什么问题

有人可能担心的一件事是,在幕后,为每个 lambda 创建站点(使用当前实现)生成一个 class,因此如果有问题的代码只执行一次,这可能被认为是一种资源浪费。这与 lambda 表达式与普通命令式代码相比具有更高的初始化开销这一事实相协调(我们在这里不与内部 classes 进行比较),因此在 class 初始化器内部,只有 运行曾经,您可能会考虑避免它。这也符合你应该的事实,所以这个潜在的优势在这里是没有的。

对于可能被JVM 优化的普通、频繁执行的代码,不会出现这些问题。正如您猜想的那样,为 lambda 表达式生成的 classes 得到与其他 classes 相同的处理(优化)。在这些地方,对集合调用 forEachfor 循环具有 更高效 的潜力。

Iterator 或 lambda 表达式创建的临时对象实例可以忽略不计,但是,可能值得注意的是 foreach 循环将始终创建一个 Iterator 实例,而 lambda expression do not always do.虽然 Iterable.forEachdefault 实现也将创建一个 Iterator,但一些最常用的集合借此机会提供专门的实现,最著名的是 ArrayList

ArrayListforEach 基本上是对数组的 for 循环,没有任何 Iterator。然后它将调用 Consumeraccept 方法,这将是一个生成的 class 包含对包含您的 lambda 表达式代码的合成方法的简单委托。为了优化整个循环,优化器的范围必须跨越数组上的 ArrayList 循环(优化器可识别的常见习惯用法),合成的 accept 方法包含一个简单的委托和包含您的实际代码的方法。

相比之下,当使用 foreach 循环遍历同一个列表时,会创建一个包含 ArrayList 迭代逻辑的 Iterator 实现,分布在两个方法中,hasNext()next()Iterator的实例变量。循环将重复调用 hasNext() 方法来检查结束条件 (index<size) 和 next() 将在返回元素之前 重新检查 条件,因为不能保证调用者在 next() 之前正确调用 hasNext()。当然,优化器能够消除这种重复,但这比一开始就没有它需要更多的努力。因此,为了获得与 forEach 方法相同的性能,优化器的范围必须跨越循环代码、非平凡的 hasNext() 实现和非平凡的 next() 实现。

类似的事情可能也适用于具有专门 forEach 实现的其他集合。这也适用于 Stream 操作,如果源提供了一个专门的 Spliterator 实现,它不会像 Iterator.

那样将迭代逻辑分散到两个方法上

因此,如果您想讨论 foreach 与 forEach(…) 的技术方面,您可以使用这些信息。

但如前所述,这些方面仅描述潜在的性能方面,因为优化器的工作和其他 运行时间环境方面可能会完全改变结果。我认为,根据经验,循环body/action越小,forEach方法越合适。这与无论如何都要避免过长的 lambda 表达式的准则完全一致。