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

时间:2016-06-29 06:44:59

标签: java lambda jvm java-8 iteration

自从他引入Java 8以来,我真的迷上了lambdas,并且尽可能地开始使用它们,主要是为了开始习惯它们。最常见的用法之一是当我们想要迭代并对一组对象进行操作时,我会使用forEachstream()。我很少写旧的for(T t : Ts)循环,我几乎忘记了for(int i = 0.....)

然而,我们前几天与我的主管讨论过这个问题,他告诉我,lambdas并不总是最好的选择,有时会妨碍表现。从我在这个新功能上看过的一个讲座中我感觉lambda迭代总是被编译器完全优化,并且(总是?)比裸迭代更好,但他不同意。这是真的?如果是,我如何区分每种方案中的最佳解决方案?

P.S:我谈论建议应用parallelStream的情况。显然这些会更快。

2 个答案:

答案 0 :(得分:2)

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

有人可能害怕的一件事是,在幕后,为每个lambda创建站点(使用当前实现)生成一个类,因此如果所讨论的代码只执行一次,这可能被视为浪费资源。这与lambda表达式具有比普通命令式代码更高的初始化开销这一事实相协调(我们不在这里比较内部类),因此在只运行一次的类初始化程序中,您可以考虑避免它。这也符合你应该never use parallel streams in class initializers的事实,所以无论如何这里都没有这种潜在优势。

对于可能由JVM优化的普通,频繁执行的代码,不会出现这些问题。正如您所说的那样,为lambda表达式生成的类与其他类获得相同的处理(优化)。在这些地方,对集合调用forEach具有效率的潜力,而不是for循环。

Iterator或lambda表达式创建的临时对象实例可以忽略不计,但是,值得注意的是foreach循环将始终创建Iterator实例而lambda expression do not always do。虽然default的{​​{1}}实施也将创建Iterable.forEach,但一些最常用的集合借此机会提供专门的实施,最明显的是Iterator。< / p>

ArrayList的{​​{1}}基本上是数组上的ArrayList循环,没有任何forEach。然后它将调用for的{​​{1}}方法,该方法将是一个生成的类,包含对包含lambda表达式代码的合成方法的简单委派。为了优化整个循环,优化器的范围必须跨越数组上的Iterator循环(优化器可识别的常用习语),包含普通委托的合成accept方法和包含实际代码的方法。

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

类似的事情也可能适用于具有专门next()实现的其他集合。如果源提供专门的forEach实现,这也适用于Stream操作,该实现不会将迭代逻辑扩展到两个方法,如hasNext()

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

但如上所述,这些方面仅描述潜在的性能方面,因为优化器的工作和其他运行时环境方面可能完全改变结果。我认为,根据经验,循环体/动作越小,Spliterator方法越合适。这完全符合避免过长的lambda表达式的指导原则。

答案 1 :(得分:1)

这取决于具体的实施。

通常forEach方法和foreach循环Iterator通常具有非常相似的性能,因为它们使用相似的抽象级别。 stream()通常较慢(通常为50-70%),因为它增加了另一个级别,可以访问底层集合。

stream()的优点通常是JDK提供的许多可重用的操作可能并行且易于链接。