Java 8 Iterable.forEach()与foreach循环
以下哪个更适合于Java 8?
Java 8:
joins.forEach(join -> mIrc.join(mSession, join));
Java 7:
for (String join : joins) {
mIrc.join(mSession, join);
}
我有很多for循环可以用lambdas“简化”,但是真的有使用它们的优点,包括性能和可读性吗?
编辑
我也将这个问题扩展到更长的方法 - 我知道你不能从lambda返回或破坏父函数,应该提及它们是否被比较,但是还有什么需要考虑的?
当这些操作可以并行执行时,这个优点就会被考虑进去。 (请参阅http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and-关于内部和外部迭代的部分)
从我的观点来看,主要优点是可以定义在循环内执行什么操作,而无需决定它是并行执行还是顺序执行
如果你想让你的循环并行执行,你可以简单地写
joins.parallelStream().forEach((join) -> mIrc.join(mSession, join));
你将不得不为线程处理等编写一些额外的代码。
注意:对于我的答案,我假设连接实现java.util.Stream
接口。 如果连接仅实现java.util.Iterable
接口,则不再成立。
更好的做法是使用for-each
。 除了违反Keep It Simple,愚蠢的原则外,对于每个forEach()
的新事物至少存在以下缺陷:
不能使用非最终变量 。 所以,下面的代码不能变成forEach lambda:
Object prev = null;
for(Object curr : list)
{
if( prev != null )
foo(prev, curr);
prev = curr;
}
无法处理检查的异常 。 Lambdas实际上并不禁止抛出检查异常,但像Consumer
这样的常用功能接口不会声明任何异常。 因此,引发checked异常的代码必须将它们封装在try-catch
或Throwables.propagate()
。 但即使你这样做了,抛出异常会发生什么并不总是很清楚。 它可能会被吞咽在forEach()
的forEach()
某处
有限的流量控制 。 lambda中的return
等于for-each中的continue
,但没有与break
相等的效果。 执行返回值,短路或设置标志也很困难(如果它不违反非最终变量规则,那么它可以缓解一些问题)。 “这不仅仅是一种优化,而且当你考虑到某些序列(如读取文件中的行)可能有副作用,或者你可能有无限的序列时,这一点很重要。”
可能并行执行 ,这对所有人来说都是一个可怕的,可怕的事情,但是需要优化的代码只有0.1%。 任何并行代码都必须考虑(即使它不使用传统多线程执行的锁,挥发物和其他特别令人讨厌的方面)。 任何bug都很难找到。
可能会影响性能 ,因为JIT无法优化forEach()+ lambda到与普通循环相同的程度,特别是现在lambda表达式是新的。 通过“优化”,我不是指调用lambdas(这很小)的开销,而是指现代JIT编译器在运行代码时执行的复杂分析和转换。
如果您确实需要并行性,那么使用ExecutorService可能要快得多并且不会太困难 。 流是自动的(阅读:不太了解你的问题),并使用专门的(阅读:低效的一般情况下)并行化策略(fork-join递归分解)。
由于嵌套的调用层次结构和上帝禁止的并行执行, 使得调试更加混乱 。 调试器可能会显示来自周围代码的变量的问题,而像逐步扫描这样的内容可能无法按预期工作。
一般来说,流编码,读取和调试都比较困难 。 实际上,对于复杂的“流利”API一般来说也是如此。 复杂的单一语句的组合,泛型的大量使用以及缺乏中间变量共同产生令人困惑的错误消息并妨碍调试。 而不是“这种方法没有类型X的重载”,你会得到一个更接近“某处弄糟类型的地方”的错误消息,但我们不知道在哪里或如何。 同样,在代码分解为多个语句时,您无法像调试器一样简单地检查事件,并将中间值保存到变量中。 最后,阅读代码并理解每个执行阶段的类型和行为可能是不平凡的。
坚持像拇指疼痛 。 Java语言已经有for-each语句。 为什么用函数调用来替代它? 为什么要鼓励在表达式的某处隐藏副作用? 为什么要鼓励笨重的单线队员? 定期为每个和每个forEach威利尼利混合是不好的风格。 代码应该用成语表达(由于它们的重复可以快速理解的模式),并且使用更少的成语使代码更清晰,并且花费更少的时间来决定使用哪个习惯用法(对于像我这样的完美主义者来说,这是一个很大的时间浪费! )。
正如你所看到的,除了有意义的情况外,我不是forEach()的忠实粉丝。
对我来说特别令人讨厌的是Stream
没有实现Iterable
(尽管实际上有方法iterator
),并且不能用于for-each,只能用forEach()。 我建议使用(Iterable<T>)stream::iterator
将Streams转换为Iterables。 更好的选择是使用StreamEx修复了许多Stream API问题,包括实现Iterable
。
也就是说, forEach()
对于以下内容很有用:
原子迭代一个同步列表 。 在此之前,使用Collections.synchronizedList()
生成的列表对于像get或set这样的东西是原子的,但在迭代时不是线程安全的。
并行执行(使用适当的并行流) 。 如果您的问题与Streams和Spliterator中内置的性能假设相匹配,这可以为您节省几行代码vs使用ExecutorService。
与同步列表一样, 特定的容器可以从迭代的控制中受益(尽管这在很大程度上是理论上的,除非人们可以提供更多示例)
通过使用forEach()
和方法引用参数(即list.forEach (obj::someMethod)
) 更简洁地调用单个函数 。 但是,请记住检查异常时的要点,更难的调试以及减少编写代码时使用的成语数量。
我用作参考的文章:
编辑:看起来像lambdas的一些原始提案(如http://www.javac.info/closures-v06a.html)解决了我提到的一些问题(当然加入自己的复杂性)。
在阅读这个问题时,人们可以得到这样的印象: Iterable#forEach
与lambda表达式结合是编写传统for-each循环的捷径/替代品。 这是不正确的。 OP的这段代码:
joins.forEach(join -> mIrc.join(mSession, join));
不是作为写作的捷径
for (String join : joins) {
mIrc.join(mSession, join);
}
并且当然不应该以这种方式使用。 相反,它的目的是作为写作的捷径(虽然它不完全相同)
joins.forEach(new Consumer<T>() {
@Override
public void accept(T join) {
mIrc.join(mSession, join);
}
});
它可以替代以下Java 7代码:
final Consumer<T> c = new Consumer<T>() {
@Override
public void accept(T join) {
mIrc.join(mSession, join);
}
};
for (T t : joins) {
c.accept(t);
}
如上例所示,用功能接口代替循环体,使代码更加明确:(1)循环的主体不影响周围的代码和控制流;(2)循环体可以用不同的函数实现代替,而不影响周围的代码。 不能访问外部作用域的非最终变量并不是函数/ lambda表达式的缺陷,而是将Iterable#forEach
的语义与传统for-each循环的语义区分开来的一个特性 。 一旦习惯了Iterable#forEach
的语法,它会使代码更具可读性,因为您可以立即获得有关代码的附加信息。
对于每个循环而言,传统的做法肯定会保留在Java中的良好实践(以避免过度使用的术语“最佳实践”)。 但这并不意味着, Iterable#forEach
应该被认为是不好的做法或不好的风格。 使用合适的工具来完成这项工作总是一种很好的做法,这包括将传统的for-each循环与Iterable#forEach
,这是合理的。
由于Iterable#forEach
的缺点已经在这个线程中讨论过了,下面是一些原因,为什么你可能想要使用Iterable#forEach
:
为了让你的代码更加明确:如上所述,在某些情况下, Iterable#forEach
可以使你的代码更加明确和可读。
使你的代码更具可扩展性和可维护性:将一个函数作为循环的主体使你可以用不同的实现来替换这个函数(参见策略模式)。 您可以轻松地用方法调用替换lambda表达式,这可能会被子类覆盖:
joins.forEach(getJoinStrategy());
然后,您可以使用枚举实现功能界面来提供默认策略。 这不仅使您的代码更具可扩展性,而且还增加了可维护性,因为它将循环实现从循环声明中分离出来。
为了使你的代码更具可调试性:从声明中分离循环实现也可以使调试变得更容易,因为你可以有专门的调试实现,打印出调试消息,而不需要用if(DEBUG)System.out.println()
。 调试实现可以例如是一个委托,用于装饰实际的功能实现。
优化性能关键代码:与此线程中的一些断言相反,至少在使用ArrayList并以“-client”模式运行Hotspot时, Iterable#forEach
已经比传统的for-each循环提供了更好的性能。 虽然这种性能提升对于大多数使用情况来说很小并且可以忽略不计,但在某些情况下,这种额外的性能可能会产生影响。 例如,如果库中的某些现有循环实现应该用Iterable#forEach
替换,那么库维护者肯定会评估它。
为了支持事实,我用Caliper做了一些微观基准测试。 这里是测试代码(需要git最新的Caliper):
@VmOptions("-server")
public class Java8IterationBenchmarks {
public static class TestObject {
public int result;
}
public @Param({"100", "10000"}) int elementCount;
ArrayList<TestObject> list;
TestObject[] array;
@BeforeExperiment
public void setup(){
list = new ArrayList<>(elementCount);
for (int i = 0; i < elementCount; i++) {
list.add(new TestObject());
}
array = list.toArray(new TestObject[list.size()]);
}
@Benchmark
public void timeTraditionalForEach(int reps){
for (int i = 0; i < reps; i++) {
for (TestObject t : list) {
t.result++;
}
}
return;
}
@Benchmark
public void timeForEachAnonymousClass(int reps){
for (int i = 0; i < reps; i++) {
list.forEach(new Consumer<TestObject>() {
@Override
public void accept(TestObject t) {
t.result++;
}
});
}
return;
}
@Benchmark
public void timeForEachLambda(int reps){
for (int i = 0; i < reps; i++) {
list.forEach(t -> t.result++);
}
return;
}
@Benchmark
public void timeForEachOverArray(int reps){
for (int i = 0; i < reps; i++) {
for (TestObject t : array) {
t.result++;
}
}
}
}
结果如下:
当使用“-client”运行时, Iterable#forEach
优于传统的for循环ArrayList,但仍然比直接遍历数组要慢。 使用“-server”运行时,所有方法的性能大致相同。
为并行执行提供可选的支持:这里已经说过,使用流并行执行Iterable#forEach
功能接口的可能性当然是一个重要的方面。 由于Collection#parallelStream()
不能保证循环实际上是并行执行的,所以必须将其视为可选功能。 用list.parallelStream().forEach(...);
迭代你的列表list.parallelStream().forEach(...);
,你明确地说:这个循环支持并行执行,但它不依赖于它。 再次,这是一个功能,而不是赤字!
通过将并行执行的决定从实际的循环实现中移出来,您可以选择优化代码,而不会影响代码本身,这是一件好事。 另外,如果默认的并行流实现不符合您的需求,则没有人阻止您提供您自己的实现。 例如,您可以根据底层操作系统,集合的大小,核心数量以及某些首选项设置提供优化的集合:
public abstract class MyOptimizedCollection<E> implements Collection<E>{
private enum OperatingSystem{
LINUX, WINDOWS, ANDROID
}
private OperatingSystem operatingSystem = OperatingSystem.WINDOWS;
private int numberOfCores = Runtime.getRuntime().availableProcessors();
private Collection<E> delegate;
@Override
public Stream<E> parallelStream() {
if (!System.getProperty("parallelSupport").equals("true")) {
return this.delegate.stream();
}
switch (operatingSystem) {
case WINDOWS:
if (numberOfCores > 3 && delegate.size() > 10000) {
return this.delegate.parallelStream();
}else{
return this.delegate.stream();
}
case LINUX:
return SomeVerySpecialStreamImplementation.stream(this.delegate.spliterator());
case ANDROID:
default:
return this.delegate.stream();
}
}
}
这里的好处是,你的循环实现不需要知道或关心这些细节。
上一篇: Java 8 Iterable.forEach() vs foreach loop
下一篇: O summary for Java Collections Framework implementations?