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-catch
或Throwables.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#forEach
function接口的可能性当然是一个重要的方面。 由于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; DR : List.stream().forEach()
是最快的。
我觉得我应该从基准迭代中增加我的结果。 我采取了一个非常简单的方法(没有基准testing框架),并以5种不同的方法为基准:
- 经典
for
- 经典的foreach
-
List.forEach()
-
List.stream().forEach()
-
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->{})