自从他引入Java 8以来,我真的迷上了lambdas,并且尽可能地开始使用它们,主要是为了开始习惯它们。最常见的用法之一是当我们想要迭代并对一组对象进行操作时,我会使用forEach
或stream()
。我很少写旧的for(T t : Ts)
循环,我几乎忘记了for(int i = 0.....)
。
然而,我们前几天与我的主管讨论过这个问题,他告诉我,lambdas并不总是最好的选择,有时会妨碍表现。从我在这个新功能上看过的一个讲座中我感觉lambda迭代总是被编译器完全优化,并且(总是?)比裸迭代更好,但他不同意。这是真的?如果是,我如何区分每种方案中的最佳解决方案?
P.S:我不谈论建议应用parallelStream
的情况。显然这些会更快。
答案 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提供的许多可重用的操作可能并行且易于链接。