Java 8 Iterable.forEach()vs foreach循环

时间:2013-05-19 13:57:23

标签: java for-loop java-8 java-stream

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

Java 8:

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

Java 7:

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

我有很多for循环可以用lambdas“简化”,但使用它们真的有什么好处吗?它会改善他们的表现和可读性吗?

修改

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

8 个答案:

答案 0 :(得分:447)

更好的做法是使用for-each。除了违反 Keep It Simple,Stupid 原则之外,新的forEach()至少还有以下不足之处:

  • 无法使用非最终变量。因此,像下面的代码不能变成forEach lambda:

    Object prev = null;
    for(Object curr : list)
    {
        if( prev != null )
            foo(prev, curr);
        prev = curr;
    }
    
  • 无法处理已检查的例外。 Lambdas实际上并未被禁止抛出已检查的异常,但Consumer之类的常见功能接口并未声明任何异常。因此,任何抛出已检查异常的代码都必须将它们包装在try-catchThrowables.propagate()中。但即使你这样做,也不总是清楚抛出的异常会发生什么。它可能会被forEach()

  • 的内脏吞噬
  • 限制流量控制。 lambda中的return等于for-each中的continue,但没有等同于break。它也很难做返回值,短路或设置标志这些事情(如果它没有违反 no,这会减轻一些事情。非最终变量规则)。 "This is not just an optimization, but critical when you consider that some sequences (like reading the lines in a file) may have side-effects, or you may have an infinite sequence."

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

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

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

  • 使调试更加混乱,因为嵌套的调用层次结构和上帝禁止并行执行。调试器可能在显示来自周围代码的变量时出现问题,而诸如步进之类的事情可能无法按预期工作。

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

  • 伸出拇指。 Java语言已经有for-each语句。为什么用函数调用替换它?为什么鼓励在表达式的某处隐藏副作用?为什么鼓励笨重的单行?定期为每个和每个新的forWach willy-nilly混合是糟糕的风格。代码应该用成语(由于重复而快速理解的模式),并且使用的习语越少,代码就越清晰,花在决定使用哪种习语上的时间就越少(对于像我这样的完美主义者来说,这是一个很大的时间流逝! )。

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

特别冒犯我的是Stream没有实现Iterable(尽管实际上有方法iterator)并且不能在for-each中使用,只能使用forEach( )。我建议使用(Iterable<T>)stream::iterator将Streams转换为Iterables。更好的选择是使用StreamEx修复许多Stream API问题,包括实施Iterable

尽管如此,forEach()对以下内容非常有用:

  • 以原子方式迭代同步列表。在此之前,使用Collections.synchronizedList()生成的列表与get或set之类的内容相比是原子的,但在迭代时不是线程安全的。

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

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

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

我用作参考的文章:

编辑:看起来lambda的一些原始提案(例如http://www.javac.info/closures-v06a.html)解决了我提到的一些问题(当然,在添加自己的复杂功能时)。

答案 1 :(得分:168)

当操作可以并行执行时,会考虑优势。 (参见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接口,则不再适用。

答案 2 :(得分:97)

在阅读这个问题时,人们可以得到这样的印象,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)循环体可以用函数的不同实现替换,而不影响周围的代码。无法访问外部作用域的非最终变量不是函数/ lambdas的缺陷,它是一个特性,它将Iterable#forEach的语义与传统的语义区分开来 - 每个循环。一旦习惯了Iterable#forEach的语法,它就会使代码更具可读性,因为您可以立即获得有关代码的其他信息。

传统的for-each循环肯定会保持良好实践(以避免Java中过度使用的术语&#34; best practice&#34;)。但这并不意味着,Iterable#forEach应被视为不良做法或不良风格。使用正确的工具来完成这项工作总是很好的做法,这包括将传统的for-each循环与Iterable#forEach混合,这是有意义的。

由于Iterable#forEach的缺点已经在这个帖子中讨论过,这里有一些原因,为什么你可能想要使用Iterable#forEach

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

  • 使您的代码更具可扩展性和可维护性:使用函数作为循环体允许您使用不同的实现替换此函数(请参阅Strategy Pattern)。你可以,例如使用方法调用轻松替换lambda表达式,可能会被子类覆盖:

    joins.forEach(getJoinStrategy());
    

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

  • 使代码更易于调试:从声明中分离循环实现也可以使调试更容易,因为您可以使用专门的调试实现,打印出调试消息,需要使用if(DEBUG)System.out.println()来混淆主代码。调试实现可以​​是例如是delegatedecorates实际的功能实现。

  • 优化性能关键代码:与此线程中的某些断言相反,Iterable#forEach 已经提供了比传统更好的性能for-each循环,至少在使用ArrayList并在&#34; -client&#34;中运行Hotspot时模式。虽然这种性能提升很小并且对于大多数用例来说可以忽略不计,但在某些情况下,这种额外的性能可能会有所不同。例如。图书馆维护者肯定想要评估,如果他们现有的一些循环实现应该用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++;
                }
            }
        }
    }
    

    以下是结果:

    使用&#34; -client&#34;运行时,Iterable#forEach优于ArrayList上的传统for循环,但仍然比直接迭代数组慢。使用&#34; -server&#34;运行时,所有方法的性能大致相同。

  • 为并行执行提供可选支持:这里已经说过,可以使用streams并行执行Iterable#forEach的功能接口,肯定是一个重要方面。由于Collection#parallelStream()不保证,循环实际上是并行执行的,因此必须将此视为可选功能。通过使用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();
            }
        }
    }
    

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

答案 3 :(得分:12)

forEach()可以实现为比for-each循环更快,因为iterable知道迭代其元素的最佳方式,而不是标准迭代器方式。所以区别在于内部循环或外部循环。

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

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

而不是需要大量脚手架的for-each循环

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

但是,我们还需要使用forEach()来计算两个开销成本,一个是制作lambda对象,另一个是调用lambda方法。它们可能并不重要。

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

答案 4 :(得分:7)

TL; DR List.stream().forEach()是最快的。

我觉得我应该从基准测试迭代中添加我的结果。 我采用了一种非常简单的方法(没有基准测试框架)并对5种不同的方法进行了基准测试:

  1. classic for
  2. 经典的foreach
  3. List.forEach()
  4. List.stream().forEach()
  5. List.parallelStream().forEach
  6. 测试程序和参数

    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)应用于其所有成员。 在Main类中,我运行三次测试方法来预热JVM。然后我运行测试方法1000次,总结每次迭代方法所花费的时间(使用System.nanoTime())。在那之后,我将该总和除以1000,结果是平均时间。 例如:

    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.8.0_05

    运行它

    经典for

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

    执行时间:4.21 ms

    经典的foreach

    for(Integer i : list) {
        doIt(i);
    }
    

    执行时间:5.95毫秒

    List.forEach()

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

    执行时间:3.11毫秒

    List.stream().forEach()

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

    执行时间:2.79 ms

    List.parallelStream().forEach

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

    执行时间:3.6毫秒

答案 5 :(得分:4)

我觉得我需要稍微扩展我的评论......

关于paradigm \ style

这可能是最值得注意的方面。 FP因为你可以避免副作用而变得流行。我不会深入研究你可以从中获得什么,因为这与问题无关。

但是,我会说使用Iterable.forEach的迭代是受到FP的启发,而不是将更多的FP带到Java的结果(具有讽刺意味的是,我说在纯FP中没有太多用于forEach,因为它确实如此除引入副作用外什么都没有。)

最后我会说这是你正在写作的品味\风格\范式的问题。

关于并行性。

从性能的角度来看,使用Iterable.forEach而不是foreach(...)没有任何明显的好处。

根据官方docs on Iterable.forEach

  

对Iterable的内容执行给定的操作,   迭代时出现订单元素,直到所有元素都已存在   已处理或操作引发异常。

...即文档非常清楚,没有隐含的并行性。添加一个就是LSP违规。

现在,Java 8中承诺提供了“并行集合”,但是要更加明确地使用那些你需要的东西,并使用它们(请参阅mschenk74的答案)。

BTW:在这种情况下,Stream.forEach将被使用,并不能保证实际工作将在并行中完成(取决于底层集合)。

更新:可能不是那么明显,而且一目了然,但还有另一个方面的风格和可读性视角。

首先 - 普通的老式forloops很简单。每个人都已经知道了。

其次,更重要的是 - 你可能只想将Iterable.forEach用于单行lambda。如果“身体”变得更重 - 它们往往不那么可读。 你有2个选项 - 使用内部类(yuck)或使用普通的旧forloop。 当人们看到相同的事物(iteratins over collections)在同一个代码库中完成不同的vays / style时,人们常常感到恼火,这似乎就是这种情况。

同样,这可能是也可能不是问题。取决于处理代码的人。

答案 6 :(得分:4)

最令人瞩目的功能forEach限制之一是缺少经过检查的异常支持。

一个possible workaround是用普通的旧foreach循环替换终端forEach

    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 function that throws exception?

Java 8: Lambda-Streams, Filter by Method with Exception

How can I throw CHECKED exceptions from inside Java 8 streams?

Java 8: Mandatory checked exceptions handling in lambda expressions. Why mandatory, not optional?

答案 7 :(得分:2)

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

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

请看下面的代码段,

  • 这里我创建了一个新的Class,它将覆盖Consumer Class中的accept类方法,         你可以在哪里添加额外的功能,超过迭代.. !!!!!!

    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);
                }
            });
        }
    }