Java 8 Iterable.forEach()vs foreach循环

Java 8 Iterable.forEach() vs foreach loop

下面哪一个在Java 8中更好的实践?

Java 8:

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

Java 7:

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

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

编辑

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


更好的做法是使用for-each。新仿制的forEach()除了违反简单、愚蠢的原则外,还至少存在以下不足:好的。

  • 不能使用非最终变量。因此,不能将如下代码转换为foreach lambda:好的。

    1
    2
    3
    4
    5
    6
    7
    Object prev = null;
    for(Object curr : list)
    {
        if( prev != null )
            foo(prev, curr);
        prev = curr;
    }
  • 无法处理选中的异常。实际上并没有禁止lambda抛出检查过的异常,但是像Consumer这样的公共功能接口不会声明任何异常。因此,任何抛出检查异常的代码都必须将它们包装在try-catchThrowables.propagate()中。但即使这样做,也不总是清楚抛出的异常会发生什么。它可能会在江户的某个地方被吞食。好的。

  • 有限流量控制。lambda中的return等于a中的continue,但没有等价于break。此外,还很难执行诸如返回值、短路或设置标志之类的操作(如果不是违反无最终变量规则,这会稍微减轻一些问题)。这不仅是一种优化,而且当您认为某些序列(如读取文件中的行)可能有副作用,或者您可能有一个无限序列时,这一点非常重要。"好的。

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

  • 可能会影响性能,因为JIT无法将foreach()+lambda优化到与普通循环相同的程度,特别是现在lambda是新的。所谓"优化",不是指调用lambda的开销(这是很小的),而是指现代JIT编译器在运行代码时执行的复杂分析和转换。好的。

  • 如果您确实需要并行,那么使用ExecutorService可能会更快,也不会更困难。流都是自动的(读:不太了解您的问题),并使用专门的(读:一般情况下效率低下)并行化策略(fork-join递归分解)。好的。

  • 由于嵌套的调用层次结构和并行执行,使得调试更加混乱。调试器在显示来自周围代码的变量时可能会遇到问题,像单步执行这样的操作可能无法按预期工作。好的。

  • 流通常更难编码、读取和调试。实际上,对于复杂的"流畅的"API来说,这是正确的。复杂的单语句、大量使用泛型和缺少中间变量的组合,共同产生令人困惑的错误消息并阻碍调试。与"此方法没有X类型的重载"不同,您会得到一条错误消息,它更接近于"在某个地方您弄乱了类型,但我们不知道在哪里或如何处理。"同样,您不能像在代码被分成多个语句时那样轻松地在调试器中单步执行和检查,并且中间值被保存到变量中。最后,阅读代码并了解执行的每个阶段的类型和行为可能是非常重要的。好的。

  • 像拇指痛一样伸出来。Java语言已经为每个语句提供了。为什么用函数调用替换它?为什么要鼓励把副作用隐藏在表达的某个地方?为什么要鼓励笨拙的一行呢?随意地为每个新的for each混合规则是不好的风格。代码应该用成语(由于重复而容易理解的模式)来表达,并且使用的成语越少,代码越清晰,决定使用哪种成语的时间就越少(对于像我这样的完美主义者来说,这是一个巨大的时间消耗!).好的。

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

特别让我感到不快的是,Stream没有实现Iterable(尽管实际上有方法iterator),并且不能在for each中使用,只使用foreach()。我建议使用(Iterable)stream::iterator将流铸造到iterables中。更好的选择是使用streamex,它修复了许多流API问题,包括实现Iterable。好的。

也就是说,forEach()可用于以下方面:好的。

  • 在同步列表上进行原子式迭代。在此之前,使用Collections.synchronizedList()生成的列表在get或set等方面是原子的,但在迭代时不是线程安全的。好的。

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

  • 特定的容器,比如同步列表,从控制迭代中受益(尽管这在很大程度上是理论上的,除非人们能举出更多的例子)。好的。

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

参考文献:好的。

  • 关于Java 8的一切
  • 内外循环(如另一张海报所示)

编辑:看起来像一些最初的关于lambdas的建议(如http://www.javac.info/closures-v06a.html)解决了我提到的一些问题(当然,这也增加了它们自己的复杂性)。好的。好啊。


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

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

  • 如果希望循环并行执行,则只需编写

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

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

注意:对于我的答案,我假设join实现java.util.Stream接口。如果join只实现java.util.Iterable接口,则不再是这样。


在阅读这个问题时,人们会有这样的印象:Iterable#forEach与lambda表达式结合使用是为每个循环编写传统的快捷方式/替代方法。这根本不是真的。此代码来自OP:好的。

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

不是写东西的捷径好的。

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

当然不应该这样使用。相反,它的目的是作为一个快捷方式(尽管它并不完全相同)来编写好的。

1
2
3
4
5
6
joins.forEach(new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
});

它取代了下面的Java 7代码:好的。

1
2
3
4
5
6
7
8
9
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循环的语义区分开来的特性。一旦习惯了EDOCX1的语法(0),代码就更可读了,因为您可以立即获得关于代码的额外信息。好的。

传统的每一个循环肯定会保持良好的做法(以避免过度使用的术语"最佳实践")在爪哇。但这并不意味着,Iterable#forEach应该被视为不好的做法或不好的风格。使用正确的工具来完成这项工作始终是一种良好的实践,这包括将每个循环的传统方法与Iterable#forEach混合在一起,这是有意义的。好的。

由于本文已经讨论了Iterable#forEach的缺点,下面是一些原因,您可能希望使用Iterable#forEach的原因:好的。

  • 为了使您的代码更显式:如上所述,Iterable#forEach可以使您的代码在某些情况下更显式和可读。好的。

  • 为了使代码更具可扩展性和可维护性:使用一个函数作为循环的主体,可以用不同的实现替换这个函数(参见策略模式)。例如,可以用方法调用轻松替换lambda表达式,这些方法调用可能会被子类覆盖:好的。

    1
    joins.forEach(getJoinStrategy());

    然后,可以使用枚举提供默认策略,该枚举实现了功能接口。这不仅使代码更具可扩展性,还增加了可维护性,因为它将循环实现与循环声明分离。好的。

  • 为了使代码更易于调试:将循环实现与声明分离也可以使调试更容易,因为您可以有一个专门的调试实现,它可以打印调试消息,而无需将主代码与if(DEBUG)System.out.println()混淆。调试实现可以是一个委托,用于修饰实际的函数实现。好的。

  • 优化性能关键代码:与此线程中的某些断言相反,Iterable#forEach已经为每个循环提供了比传统的更好的性能,至少在使用arraylist并以"-client"模式运行Hotspot时是如此。虽然对于大多数用例来说,这种性能提升很小,可以忽略不计,但在某些情况下,这种额外的性能可能会产生影响。例如,如果一些现有的循环实现应该替换为Iterable#forEach,库维护人员肯定会想评估。好的。

    为了用事实支持这一说法,我用卡尺做了一些微基准测试。以下是测试代码(需要Git的最新卡尺):好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    @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在arraylist上优于传统的for循环,但仍然比直接迭代数组慢。当使用"-server"运行时,所有方法的性能都差不多。好的。

  • 为并行执行提供可选支持:这里已经说过,可以使用流并行执行Iterable#forEach的功能接口,这当然是一个重要方面。由于Collection#parallelStream()不保证循环实际上是并行执行的,因此必须将其视为可选功能。通过使用list.parallelStream().forEach(...);迭代您的列表,您可以明确地说:这个循环支持并行执行,但不依赖于它。同样,这是一个特征,而不是赤字!好的。

    通过将并行执行的决定从实际的循环实现中移开,您可以在不影响代码本身的情况下对代码进行可选的优化,这是一件好事。另外,如果默认的并行流实现不适合您的需要,没有人会阻止您提供自己的实现。例如,您可以根据基础操作系统、集合大小、核心数量和某些首选项设置提供优化的集合:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    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();
            }
        }
    }

    这里的好处是,循环实现不需要知道或关心这些细节。好的。

好啊。


forEach()的实现速度比每个循环都快,因为ITerable知道迭代其元素的最佳方法,而不是标准的迭代器方法。所以区别是内部循环或外部循环。

例如,ArrayList.forEach(action)可以简单地实现为

1
2
for(int i=0; i<size; i++)
    action.accept(elements[i])

与每个需要大量脚手架的回路相反

1
2
3
4
Iterator iter = list.iterator();
while(iter.hasNext())
    Object next = iter.next();
    do something with `next`

但是,我们还需要通过使用forEach()来计算两个间接成本,一个是生成lambda对象,另一个是调用lambda方法。它们可能不重要。

另请参见http://journal.stuffwithstufacture.com/2013/01/13/iteration-inside-and-out/以比较不同用例的内部/外部迭代。


医生:List.stream().forEach()是最快的。

我觉得应该添加基准迭代的结果。我采用了一种非常简单的方法(没有基准框架),并将5种不同的方法作为基准:

  • 经典for
  • 经典前奏曲
  • List.forEach()
  • List.stream().forEach()
  • List.parallelStream().forEach
  • 测试程序和参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    private List<Integer> list;
    private final int size = 1_000_000;

    public MyClass(){
        list = new ArrayList<>();
        Random rand = new Random();
        for (int i = 0; i < size; ++i) {
            list.add(rand.nextInt(size * 50));
        }    
    }
    private void doIt(Integer i) {
        i *= 2; //so it won't get JITed out
    }

    该类中的列表应迭代,并通过不同的方法将一些doIt(Integer i)应用于它的所有成员。在主类中,我运行测试方法三次以预热JVM。然后,我运行测试方法1000次,求出每个迭代方法所需时间的总和(使用System.nanoTime())。做完之后,我把这个和除以1000,结果就是平均时间。例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    myClass.fored();
    myClass.fored();
    myClass.fored();
    for (int i = 0; i < reps; ++i) {
        begin = System.nanoTime();
        myClass.fored();
        end = System.nanoTime();
        nanoSum += end - begin;
    }
    System.out.println(nanoSum / reps);

    我在i5 4内核CPU上运行这个程序,Java版本1.80y05

    经典for

    1
    2
    3
    for(int i = 0, l = list.size(); i < l; ++i) {
        doIt(list.get(i));
    }

    执行时间:4.21 ms

    经典前奏曲

    1
    2
    3
    for(Integer i : list) {
        doIt(i);
    }

    执行时间:5.95 ms

    List.forEach()

    1
    list.forEach((i) -> doIt(i));

    执行时间:3.11 ms

    List.stream().forEach()

    1
    list.stream().forEach((i) -> doIt(i));

    执行时间:2.79 ms

    List.parallelStream().forEach

    1
    list.parallelStream().forEach((i) -> doIt(i));

    执行时间:3.6 ms


    最具升级功能的forEach的限制之一是缺少检查异常支持。

    一种可能的解决办法是用普通的ForEach回路替换终端forEach

    1
    2
    3
    4
    5
        Stream<String> stream = Stream.of("","1","2","3").filter(s -> !s.isEmpty());
        Iterable<String> iterable = stream::iterator;
        for (String s : iterable) {
            fileWriter.append(s);
        }

    以下是关于lambda和流中已检查异常处理的其他解决方法的最常见问题列表:

    抛出异常的Java 8 lambda函数?

    Java 8:lambda流,用异常筛选方法

    如何从Java 8流内部抛出检查异常?

    Java 8:在lambda表达式中处理强制检查异常。为什么是强制性的,而不是可选的?


    我觉得我需要把我的评论再扩展一点…

    关于范例风格

    这可能是最值得注意的方面。由于可以避免副作用,FP变得流行起来。我不会深入探讨你能从中得到什么好处,因为这与问题无关。

    但是,我会说使用迭代的迭代是由FP启发的,而不是将更多的FP引入Java的结果(讽刺的是,我认为在纯FP中没有太多的使用,因为它除了引入副作用之外什么都不做)。

    最后,我要说的是,这是一个相当的问题的品味风格范式你目前正在写。

    关于并行性。

    从性能的角度来看,使用iterable.foreach而不是foreach(…)并没有保证会带来显著的好处。

    根据ITerable.foreach上的官方文件:

    Performs the given action on the contents of the Iterable, in the
    order elements occur when iterating, until all elements have been
    processed or the action throws an exception.

    …也就是说,文档非常清楚,没有隐含的并行性。添加一个将是LSP冲突。

    现在,Java 8中已经承诺了"并行集合",但要与您需要的更明确地进行合作,并特别注意使用它们(例如,请参阅MSCEKK74的答案)。

    顺便说一句:在这种情况下,将使用stream.foreach,它不能保证实际工作将在parallel中完成(取决于底层集合)。

    更新:可能不那么明显,一眼看上去有点夸张,但风格和可读性视角还有另一个方面。

    首先,老一套的forloop是老一套的。每个人都已经认识他们了。

    第二,更重要的是-你可能想用iterable.foreach只与一个线性羊羔。如果"身体"变得更重——它们往往不那么可读。这里有两个选项-使用内部类(yuck)或使用普通的旧forloop。当人们看到相同的事情(对集合进行迭代)在相同的代码库中执行不同的vay/样式时,他们常常会感到恼火,而这似乎是事实。

    同样,这可能是一个问题,也可能不是。这取决于从事代码工作的人。


    Java 1.8 FoeCH方法优于1.7增强型for循环的优点是,在编写代码时,只能专注于业务逻辑。

    foreach方法将java.util.function.consumer对象作为参数,因此它有助于将我们的业务逻辑放在单独的位置,您可以随时重用它。

    看看下面的片段,

    • 在这里,我创建了一个新类,它将重写使用者类中的accept class方法,在这里你可以添加更多的功能,而不是迭代。!!!!!!!

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      class MyConsumer implements Consumer<Integer>{

          @Override
          public void accept(Integer o) {
              System.out.println("Here you can also add your business logic that will work with Iteration and you can reuse it."+o);
          }
      }

      public class ForEachConsumer {

          public static void main(String[] args) {

              // Creating simple ArrayList.
              ArrayList<Integer> aList = new ArrayList<>();
              for(int i=1;i<=10;i++) aList.add(i);

              //Calling forEach with customized Iterator.
              MyConsumer consumer = new MyConsumer();
              aList.forEach(consumer);


              // Using Lambda Expression for Consumer. (Functional Interface)
              Consumer<Integer> lambda = (Integer o) ->{
                  System.out.println("Using Lambda Expression to iterate and do something else(BI).."+o);
              };
              aList.forEach(lambda);

              // Using Anonymous Inner Class.
              aList.forEach(new Consumer<Integer>(){
                  @Override
                  public void accept(Integer o) {
                      System.out.println("Calling with Anonymous Inner Class"+o);
                  }
              });
          }
      }