Java 8 Iterable.forEach()与foreach循环

在Java 8中,以下哪项更好?

Java 8:

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

Java 7:

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

我有很多可以用lambdas“简化”的循环,但是真的有使用它们的优点,包括性能和可读性吗?

编辑

我也将这个问题延伸到更长的方法 – 我知道你不能返回或打破拉姆达的父函数,这应该提到,如果他们比较,但还有什么可以考虑?

当操作可以并行执行的时候,这个优点就被考虑进去了。 (请参阅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接口,则不再成立。

更好的做法是使用for-each 。 除了违反保持简单,愚蠢的原则之外,对于每个forEach()的新事物至less存在以下缺陷:

  • 不能使用非最终variables 。 所以,下面的代码不能变成forEach lambda:

     Object prev = null; for(Object curr : list) { if( prev != null ) foo(prev, curr); prev = curr; } 
  • 无法处理检查的exception 。 Lambdas实际上并不禁止抛出检查exception,但像Consumer这样的常见function接口不会声明任何exception。 因此,引发checkedexception的代码必须将它们封装在try-catchThrowables.propagate() 。 但即使你这样做了,抛出的exception也不一定清楚。 在forEach()它可能会被吞噬

  • 有限的stream量控制 。 lambda中的return等于for-each中的continue ,但没有与break相等的效果。 做返回值,短路或设置标志也是很困难的(如果这不是违反非最终variables规则的话,这可能会缓解一些事情)。 “这不仅仅是一种优化,而且当你考虑某些序列(比如读取文件中的行)时可能会有副作用,或者你可能有一个无限的序列。

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

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

  • 如果您确实需要并行性,那么使用ExecutorService可能要快得多,而且也不会太困难 。 stream是自动的(阅读:不太了解你的问题), 使用专门的(读:低效率的一般情况下)并行化策略( 叉联recursion分解 )。

  • 由于嵌套的调用层次结构和上帝禁止的并行执行, 使得debugging更加混乱 。 debugging器可能会显示来自周围代码的variables的问题,并且像逐步扫描一样可能无法正常工作。

  • 一般来说,stream编码,读取和debugging都比较困难 。 事实上,一般情况下,复杂的“ stream利 ”API也是如此。 复杂的单一语句的组合,generics的大量使用以及缺less中间variables会产生令人困惑的错误消息,并且会妨碍debugging。 而不是“这种方法没有typesX的重载”,你会得到一个更接近“某处弄糟types的地方”的错误消息,但我们不知道在哪里或如何。 同样,在代码分解成多个语句时,不能像debugging器那样简单地检查事物,而将中间值保存到variables中。 最后,阅读代码并理解每个执行阶段的types和行为可能是不平凡的。

  • 像拇指疼痛伸出 。 Java语言已经有for-each语句。 为什么用函数调用来replace它? 为什么鼓励在expression式的某处隐藏副作用? 为什么要鼓励笨重的单行? 定期为每一个新的和每一个无所不能的风格。 代码应该用成语expression(由于它们的重复而快速理解的模式),并且使用更less的成语使代码更清晰,并且花费更less的时间来决定使用哪个习惯用法(对于像我这样的完美主义者来说,是一个很大的时间浪费! )。

正如你所看到的,除了在合理的情况下,我不是forEach()的忠实粉丝。

对我来说特别令人讨厌的是Stream并没有实现Iterable (尽pipe实际上有方法iterator ),不能用for-each,只用forEach()。 我build议使用(Iterable<T>)stream::iterator将Streams转换为Iterables。 更好的select是使用StreamEx修复了许多Stream API问题,包括实现Iterable

这就是说, forEach()对于以下内容是有用的:

  • primefaces迭代一个同步列表 。 在此之前,使用Collections.synchronizedList()生成的列表对于像get或set这样的东西是primefaces的,但是在迭代时不是线程安全的。

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

  • 与同步列表一样, 特定的容器可以从迭代的控制中受益(尽pipe这在很大程度上是理论上的,除非人们可以举出更多的例子)

  • 通过使用forEach()和方法引用参数(即list.forEach (obj::someMethod)更简洁地调用单个函数 。 但是,要记住检查exception点,更难以debugging,并减less编写代码时使用的成语数。

我用作参考的文章:

  • 关于Java 8的一切
  • 迭代内外 (正如另一张海报所指出的那样)

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

在阅读这个问题时,人们可以得到这样的印象: Iterable#forEach和lambdaexpression式结合起来就是写一个传统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); } 

如上例所示,用function接口代替循环体,使得代码更加明确:(1)循环的主体不影响周围的代码和控制stream;(2)循环体可以replace为不同的函数实现,而不影响周围的代码。 不能访问外部作用域的非最终variables不是函数/ lambdaexpression式的缺陷,而是将Iterable#forEach的语义与传统for-each循环的语义区分开来的一个特性 。 一旦习惯了Iterable#forEach的语法,它会使代码更具可读性,因为您可以立即获得有关代码的更多信息。

对于每个循环来说,传统的做法肯定会保留在Java中的良好实践 (以避免过度使用的术语“ 最佳实践 ”)。 但这并不意味着, Iterable#forEach应该被认为是不好的做法或不好的风格。 使用正确的工具来完成工作总是一个很好的做法,这包括将传统的for-each循环与Iterable#forEach混合在一起,这是合理的。

由于Iterable#forEach的缺点已经在这个线程中讨论过了,下面是一些原因,为什么你可能想要使用Iterable#forEach

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

  • 为了使你的代码更具可扩展性和可维护性:使用一个函数作为循环的主体允许你用不同的实现来replace这个函数(参见策略模式 )。 你可以很容易地用一个方法调用replacelambdaexpression式,这个方法可以被子类覆盖:

     joins.forEach(getJoinStrategy()); 

    然后你可以使用一个枚举来提供默认的策略,它实现了function接口。 这不仅使您的代码更具可扩展性,而且还增加了可维护性,因为它将循环声明中的循环实现解耦。

  • 为了使你的代码更加可debugging:从声明中分离循环实现也可以使debugging变得更容易,因为你可以有一个专门的debugging实现,打印出debugging消息,而不需要用if(DEBUG)System.out.println() 。 debugging实现可能是一个委托 ,它装饰了实际的function实现。

  • 优化性能关键代码:与此线程中的一些断言相反, Iterable#forEach已经比传统的for-each循环提供了更好的性能,至less在使用ArrayList并以“-client”模式运行Hotspot时。 虽然这种性能提升对于大多数使用情况来说很小而且可以忽略不计,但在某些情况下,这种额外的性能可能会有所作为。 例如,如果一些现有的循环实现应该用Iterable#forEach代替,那么库维护者肯定会评估它。

    为了支持事实,我用Caliper做了一些微观基准。 这里是testing代码(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优于传统的for循环ArrayList,但仍然比直接迭代数组要慢。 运行“-server”时,所有方法的性能大致相同。

  • 为并行执行提供可选的支持:这里已经说过,使用stream执行Iterable#forEachfunction接口的可能性当然是一个重要的方面。 由于Collection#parallelStream()不能保证,循环实际上是并行执行的,所以必须考虑这是一个可选的特性。 通过迭代列表list.parallelStream().forEach(...); ,你明确地说:这个循环支持并行执行,但不依赖于它。 再次,这是一个function,而不是赤字!

    通过将并行执行的决策从实际的循环实现中移走,您可以select优化代码,而不会影响代码本身,这是一件好事。 另外,如果默认的并行stream实现不符合您的需求,则没有人阻止您提供自己的实现。 例如,您可以根据底层操作系统,集合的大小,核心数量以及某些首选项设置来提供优化的集合:

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

例如ArrayList.forEach(action)可能被简单地实现为

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

而不是每个需要大量脚手架的循环

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

但是,我们还需要通过使用forEach()来解决两个开销成本,一个是创buildlambda对象,另一个是调用lambda方法。 他们可能不重要。

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

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

我觉得我应该从基准迭代中增加我的结果。 我采取了一个非常简单的方法(没有基准testing框架),并以5种不同的方法为基准:

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

testing程序和参数

 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类中,我运行三次testing的方法来预热JVM。 然后,我运行testing方法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 ms

对于每个限制来说,最令人不快的function之一就是缺less对exception的支持。

一种可能的解决方法是用普通的旧foreach循环replaceterminal 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); } 

以下是有关在lambdas和stream中检查exception处理的其他解决方法的最常见问题的列表:

抛出exception的Java 8 Lambda函数?

Java 8:Lambdastream,按exception进行过滤

我怎样才能从Java 8stream内引发CHECKEDexception?

Java 8:在lambdaexpression式中强制检查exception处理。 为什么是强制性的,不可选

我觉得我需要扩大我的评论…

关于范式\风格

这可能是最显着的方面。 FP由于可以避免副作用而变得stream行。 我不会深入研究你能从中得到什么优点,因为这与问题无关。

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

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

关于并行性。

从性能angular度来看,使用Iterable.forEach在foreach(…)上没有任何明显的好处。

根据Iterable.forEach官方文档 :

对Iterable的内容执行给定动作,迭代时会出现元素的顺序 ,直到处理完所有元素或者引发exception。

也就是说文档非常清楚,不会有隐含的并行性。 添加一个会违反LSP。

现在,Java 8已经有了“平行集合”,但是为了更加明确地使用那些你需要的东西,并且特别注意使用它们(参见mschenk74的答案)。

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

更新:可能不是那么明显,一眼就能看出来,但是风格和可读性还有另外一个方面。

首先 – 简单的旧的forloops是平淡而古老的。 每个人都已经知道他们。

其次,也是更重要的 – 你可能只想用Iterable.forEach和单行lambdaexpression式。 如果“身体”变得更重 – 他们往往是不可读的。 你有两个select从这里 – 使用内部类(yuck)或使用普通的旧forloop。 当人们看到相同的东西(集合上的迭代)在相同的代码库中被做成各种各样的vays /样式时,经常会感到恼火,这似乎是这样的。

再次,这可能或可能不是一个问题。 取决于从事代码工作的人员。

我已经testing代码如下:

 public class TestPerformance { public static void main(String[] args) { List<Integer> numbers = getNumbers(); testJava7(numbers); testForeachJava7(numbers); testJava8(numbers); testStreamJava8(numbers); testParallelStreamJava8(numbers); } private static void testJava7(List<Integer> numbers){ long startTime = System.currentTimeMillis(); int size = numbers.size(); for(int i = 0; i < size; i++){ numbers.get(i); } long endTime = System.currentTimeMillis(); long totalTime = endTime - startTime; System.out.println("testJava7 " + totalTime + " ms"); } private static void testForeachJava7(List<Integer> numbers){ long startTime = System.currentTimeMillis(); for(Integer num : numbers){ } long endTime = System.currentTimeMillis(); long totalTime = endTime - startTime; System.out.println("testForeachJava7 " + totalTime + " ms"); } private static void testJava8(List<Integer> numbers){ long startTime = System.currentTimeMillis(); numbers.forEach(s -> {}); long endTime = System.currentTimeMillis(); long totalTime = endTime - startTime; System.out.println("testJava8 " + totalTime + " ms"); } private static void testStreamJava8(List<Integer> numbers){ long startTime = System.currentTimeMillis(); numbers.stream().forEach(s -> {}); long endTime = System.currentTimeMillis(); long totalTime = endTime - startTime; System.out.println("testStreamJava8 " + totalTime + " ms"); } private static void testParallelStreamJava8(List<Integer> numbers){ long startTime = System.currentTimeMillis(); numbers.parallelStream().forEach(s -> {}); long endTime = System.currentTimeMillis(); long totalTime = endTime - startTime; System.out.println("testParallelStreamJava8 " + totalTime + " ms"); } private static List<Integer> getNumbers(){ List<Integer> numbers = new ArrayList<>(); for (int i = 1; i<=5000000; i++){ numbers.add(i); } return numbers; } } 

结果:

 testJava7 4 ms testForeachJava7 17 ms testJava8 65 ms testStreamJava8 17 ms testParallelStreamJava8 27 ms 

As result above: the best performance is for(int i; i < size; i++) -> next to foreach(item : list) and list.stream.foreach(s->{}) -> next to list.parallelStream.foreach(s->{}) -> last is list.foreach(s->{})