518

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

爪哇 8:

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

爪哇 7:

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

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

编辑

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

4

8 回答 8

592

更好的做法是使用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-catchor中Throwables.propagate()。但即使你这样做了,也并不总是清楚抛出的异常会发生什么。它可能会被吞进肚子里的某个地方forEach()

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

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

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

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

  • 由于嵌套的调用层次结构和上帝禁止的并行执行,使调试更加混乱。调试器在显示周围代码中的变量时可能会遇到问题,并且诸如单步执行之类的操作可能无法按预期工作。

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

  • 像拇指酸痛一样伸出来。Java 语言已经有了 for-each 语句。为什么用函数调用代替它?为什么要鼓励在表达式的某处隐藏副作用?为什么要鼓励笨拙的单行字?将常规的 for-each 和新的 forEach 混用是不好的风格。代码应该用惯用语说话(由于重复而容易理解的模式),使用的惯用语越少,代码就越清晰,决定使用哪个惯用语的时间就越少(对于像我这样的完美主义者来说,这是一个很大的时间消耗! )。

如您所见,我不是 forEach() 的忠实拥护者,除非它有意义。

对我来说特别冒犯的是Stream它没有实现Iterable(尽管实际上有方法iterator)并且不能在 for-each 中使用,只能与 forEach() 一起使用。我建议使用(Iterable<T>)stream::iterator. 更好的选择是使用StreamEx,它修复了许多 Stream API 问题,包括实现Iterable.

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

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

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

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

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

我用来参考的文章:

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

于 2013-11-24T16:45:31.013 回答
173

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

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

  • 如果您希望您的循环并行执行,您可以简单地编写

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

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

注意:对于我的回答,我假设连接实现了java.util.Stream接口。如果 joins 仅实现java.util.Iterable接口,则不再适用。

于 2013-05-19T14:05:46.320 回答
122

阅读此问题时,您会得到这样的印象,即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)循环体可以替换为函数的不同实现,而不会影响周围的代码。不能访问外部范围的非 final 变量并不是函数/lambdas 的缺陷,它是一个区别于传统 for-each 循环语义的特性。Iterable#forEach一旦习惯了 的语法Iterable#forEach,它会使代码更具可读性,因为您会立即获得有关代码的附加信息。

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

由于Iterable#forEach在这个线程中已经讨论了的缺点,这里有一些原因,你可能想要使用Iterable#forEach

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

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

    joins.forEach(getJoinStrategy());
    

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

  • 使您的代码更易于调试:将循环实现与声明分开也可以使调试更容易,因为您可以拥有一个专门的调试实现,它可以打印出调试消息,而无需使用if(DEBUG)System.out.println(). 调试实现可以​​是一个委托,它装饰实际的功能实现。

  • 优化性能关键代码:与此线程中的一些断言相反,Iterable#forEach 它确实已经提供了比传统的 for-each 循环更好的性能,至少在使用 ArrayList 并在“-client”模式下运行 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++;
                }
            }
        }
    }
    

    结果如下:

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

  • 为并行执行提供可选支持:这里已经说过,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();
            }
        }
    }
    

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

于 2014-03-19T10:02:30.007 回答
13

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

例如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/以比较不同用例的内部/外部迭代。

于 2013-05-19T18:00:07.113 回答
10

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

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

  1. 经典的for
  2. 经典的 foreach
  3. List.forEach()
  4. List.stream().forEach()
  5. List.parallelStream().forEach

测试程序和参数

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 类中,我运行了 3 次测试方法来预热 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 毫秒

经典的 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 毫秒

List.parallelStream().forEach

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

执行时间:3.6 毫秒

于 2014-09-15T19:39:18.597 回答
8

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

关于范式\风格

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

但是,我会说使用 Iterable.forEach 的迭代受到 FP 的启发,而是将更多 FP 引入 Java 的结果(具有讽刺意味的是,我会说 forEach 在纯 FP 中没有多大用处,因为它除了引入副作用)。

最后,我想说的是,您当前所写的内容与品味\风格\范式有关。

关于并行。

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

根据Iterable.forEach 上的官方文档

对 Iterable 的内容执行给定的操作,按照迭代时元素发生的顺序,直到所有元素都被处理或操作引发异常。

...即文档非常清楚不会有隐含的并行性。添加一个将违反 LSP。

现在,Java 8 中承诺了“并行集合”,但要与那些你需要我更明确地使用它们并在使用它们时格外小心(例如,参见 mschenk74 的答案)。

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

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

首先 - 普通的旧 forloops 是普通的和旧的。每个人都已经认识他们了。

其次,更重要的是 - 您可能只想将 Iterable.forEach 与单行 lambda 一起使用。如果“body”变得更重——它们往往不那么可读。从这里您有 2 个选项 - 使用内部类 (yuck) 或使用普通的旧 forloop。当人们看到在同一个代码库中以不同的方式/样式完成相同的事情(迭代集合)时,人们经常会感到恼火,这似乎就是这种情况。

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

于 2013-05-19T16:31:47.590 回答
6

最令人愉快forEach的功能性限制之一是缺乏检查异常支持。

一种可能的解决方法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函数?

Java 8:Lambda-Streams,按异常方法过滤

如何从 Java 8 流中抛出 CHECKED 异常?

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

于 2015-07-29T17:55:24.540 回答
2

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

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

看看下面的片段,

  • 在这里,我创建了新的类,它将覆盖消费者类的接受类方法,您可以在其中添加额外的功能,不仅仅是迭代..!!!!!!

    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);
                }
            });
        }
    }
    
于 2017-12-23T14:59:03.053 回答