一尘不染

Java 8 Iterable.forEach()与foreach循环

java

以下哪个是Java 8中的最佳实践?

Java 8:

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

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

我有很多for循环可以使用lambda进行“简化”,但是使用它们真的有任何优势吗?会提高其性能和可读性吗?

编辑

我还将这个问题扩展到更长的方法。我知道你无法从lambda返回或中断父函数,并且在比较它们时也应考虑到这一点,但是还有什么需要考虑的吗?


阅读 1515

收藏
2020-03-05

共1个答案

一尘不染

好的做法是使用for-each。除了违反“ 保持简单,愚蠢”的原则外,新骗子forEach()还至少存在以下缺陷:

  • Can’t use non-final variables。因此,无法将以下代码转换为forEach lambda:
Object prev = null;
for(Object curr : list)
{
    if( prev != null )
        foo(prev, curr);
    prev = curr;
}
  • Can’t handle checked exceptions。实际上并未禁止Lambda引发检查异常,但是常见的功能接口(Consumer如未声明任何异常)。因此,任何引发检查异常的代码都必须将其包装在try-catch或中Throwables.propagate()。但是即使你这样做,也不总是总是清楚抛出的异常发生了什么。它可能被吞噬在forEach()

  • Limited flow-control。一个return在拉姆达等于continue在换每个,但没有相当于一个break。执行返回值,短路或设置标志之类的操作也很困难(如果这不违反“ 禁止非最终变量”规则,则可能会有所减轻)。“这不仅是一种优化,而且在你考虑某些序列(例如读取文件中的行)可能有副作用或可能有无限序列时也很关键。”

  • Might execute in parallel,这对于除了需要优化的代码的0.1%之外的所有代码来说都是一件可怕的事情。任何并行代码都必须经过仔细考虑(即使它不使用锁,volatile和传统多线程执行的其他特别讨厌的方面)。任何错误都很难找到。

  • Might hurt performance,因为JIT无法以与普通循环相同的程度优化forEach()+ lambda,尤其是现在lambda是新的。“优化”不是指调用lambda(很小)的开销,而是指现代JIT编译器对正在运行的代码执行的复杂分析和转换。

如果你确实需要并行性,则使用ExecutorService可能会更快,也不会更困难。流既是自动的(阅读:对你的问题不太了解),又使用专门的(阅读:在一般情况下效率不高)并行化策略(fork-join递归分解)。

  • 由于嵌套的调用层次结构以及令人难以置信的并行执行,使调试更加混乱。调试器可能无法显示周围代码中的变量,并且逐步调试等操作可能无法按预期进行。

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

  • Sticks out like a sore thumb。Java语言已经具有for-each语句。为什么用函数调用替换它?为什么鼓励在表达式中的某处隐藏副作用?为什么鼓励笨拙的单线?将常规的for-each和new forEach混为一谈是不好的方式。代码应该以习惯用语(由于重复而易于理解的模式)来表达,使用的习惯用语越少,代码就越清晰,决定使用哪种习惯用语的时间就越少(对于像我这样的完美主义者来说,这是一个很大的时间流失! )。

如你所见,我不喜欢forEach(),除非有必要。

对于我来说,特别令人反感的是Stream无法实现Iterable(尽管实际上具有method iterator),并且不能仅通过forEach()在for-each中使用。我建议使用将流转换为Iterables (Iterable)stream::iterator。更好的替代方法是使用StreamEx,它可以解决许多Stream API问题,包括实现Iterable。

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

  • 以原子方式对同步列表进行迭代。在此之前Collections.synchronizedList(),对于诸如get或set之类的东西,用with 生成的列表是原子的,但是在迭代时不是线程安全的。

  • 并行执行(使用适当的并行流)。如果你的问题与StreamsSpliterators中内置的性能假设相符,则与使用ExecutorService相比,这可以节省几行代码。

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

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

2020-03-05