Java 8 Iterable.forEach() vs foreach loop

2022-08-31 04:21:13

以下哪项是 Java 8 中更好的做法?

Java 8:

joins.forEach(join -> mIrc.join(mSession, join));

Java 7:

for (String join : joins) {
    mIrc.join(mSession, join);
}

我有很多可以使用lambdas“简化”的for循环,但是使用它们真的有什么好处吗?它会提高它们的性能和可读性吗?

编辑

我还将把这个问题扩展到更长的方法。我知道您不能从lambda返回或中断父函数,在比较它们时也应该考虑到这一点,但是还有其他需要考虑的吗?


答案 1

更好的做法是使用 。除了违反“保持简单,愚蠢”原则外,新奇的人至少还有以下不足之处:for-eachforEach()

  • 不能使用非最终变量。因此,像下面这样的代码不能变成 forEach lambda:
Object prev = null;
for(Object curr : list)
{
    if( prev != null )
        foo(prev, curr);
    prev = curr;
}
  • 无法处理选中的异常。Lambda 实际上并没有被禁止抛出已检查的异常,但常见的功能接口(如)不会声明任何异常。因此,任何引发已检验异常的代码都必须将它们包装在 或 中。但是,即使您这样做,也并不总是清楚引发的异常会发生什么情况。它可以被吞噬在肠道的某个地方Consumertry-catchThrowables.propagate()forEach()

  • 有限的流量控制。lambda 中的 A 等于 for-each 中的 a,但没有等效于 .做一些事情也很困难,比如返回值、短路或设置标志(如果不是违反无非最终变量规则,这会稍微缓解一些事情)。“这不仅仅是一种优化,但当你考虑到某些序列(如读取文件中的行)可能有副作用,或者你可能有一个无限的序列时,这一点至关重要。returncontinuebreak

  • 可能会并行执行,这对于所有人来说都是一件可怕的事情,除了需要优化的0.1%的代码。任何并行代码都必须经过深思熟虑(即使它不使用锁,易失性以及传统多线程执行的其他特别令人讨厌的方面)。任何错误都很难找到。

  • 可能会损害性能,因为 JIT 无法将 forEach()+lambda 优化到与普通循环相同的程度,尤其是在 lambda 是新的现在。通过“优化”,我指的不是调用lambdas的开销(这很小),而是指现代JIT编译器在运行代码时执行的复杂分析和转换。

  • 如果你确实需要并行性,那么使用执行器服务可能会更快,也不会更困难。流既是自动的(阅读:对你的问题了解不多),使用专门的(阅读:一般情况下效率低下)并行化策略(分叉连接递归分解)。

  • 使调试更加混乱,因为嵌套的调用层次结构以及并行执行(上帝禁止)。调试器在显示周围代码中的变量时可能会出现问题,并且单步执行等操作可能无法按预期工作。

  • 通常,流更难以编码、读取和调试。实际上,对于复杂的“流利”API 来说,情况确实如此。复杂的单个语句、泛型的大量使用以及缺少中间变量的组合共同产生了令人困惑的错误消息并阻碍了调试。而不是“此方法没有类型 X 的重载”,您得到的错误消息更接近于“您在某个地方搞砸了类型,但我们不知道在哪里或如何。同样,您不能像将代码分解为多个语句并且中间值保存到变量时那样轻松地在调试器中单步执行和检查内容。最后,阅读代码并了解每个执行阶段的类型和行为可能并非易事。

  • 像一个疼痛的拇指一样伸出来。Java 语言已经具有 for-each 语句。为什么要用函数调用替换它?为什么要鼓励在表情的某个地方隐藏副作用?为什么要鼓励笨拙的单行本?混合常规的 for-each 和 new forEach 随意无力是糟糕的风格。代码应该用习语(由于重复而快速理解的模式)说话,使用习惯用语越少,代码就越清晰,决定使用哪个习语所花费的时间就越少(对于像我这样的完美主义者来说,这是一个很大的时间消耗!)。

正如你所看到的,我不是forEach()的忠实粉丝,除非它有意义。

对我来说特别令人反感的是,它不实现(尽管实际上有方法),并且不能用于for-each,只能与forEach()一起使用。我建议使用 将 Streams 转换为 迭代对象。更好的替代方法是使用StreamEx,它修复了许多Stream API问题,包括实现。StreamIterableiterator(Iterable<T>)stream::iteratorIterable

也就是说,对于以下方面很有用:forEach()

  • 以原子方式循环访问同步列表。在此之前,与 生成的列表对于 get 或 set 等内容是原子的,但在迭代时不是线程安全的。Collections.synchronizedList()

  • 并行执行(使用适当的并行流)。与使用 ExecutorService 相比,这为您节省了几行代码(如果您的问题与流和拆分器中内置的性能假设相匹配)。

  • 特定的容器,如同步列表,受益于对迭代的控制(尽管这在很大程度上是理论上的,除非人们可以提出更多的例子)

  • 通过使用和方法引用参数(即)更干净地调用单个函数。但是,请记住有关已检查异常、更困难的调试以及减少编写代码时使用的习惯用语数量的要点。forEach()list.forEach (obj::someMethod)

我用来参考的文章:

编辑:看起来一些关于lambdas的原始提案(例如 http://www.javac.info/closures-v06a.html Google Cache)解决了我提到的一些问题(当然,同时增加了自己的复杂性)。


答案 2

当操作可以并行执行时,优势就会被考虑在内。(请参阅 http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and - 有关内部和外部迭代的部分)

  • 从我的角度来看,主要优点是可以定义在循环中要执行的操作的实现,而不必决定它是并行执行还是顺序执行。

  • 如果你想让你的循环并行执行,你可以简单地写

     joins.parallelStream().forEach(join -> mIrc.join(mSession, join));
    

    您将必须为线程处理等编写一些额外的代码。

注意:对于我的答案,我假设连接实现了接口。如果联接仅实现接口,则不再为真。java.util.Streamjava.util.Iterable