以下哪项是 Java 8 中更好的做法?
爪哇 8:
joins.forEach(join -> mIrc.join(mSession, join));
爪哇 7:
for (String join : joins) {
mIrc.join(mSession, join);
}
我有很多 for 循环可以用 lambdas “简化”,但是使用它们真的有什么好处吗?它会提高它们的性能和可读性吗?
编辑
我还将这个问题扩展到更长的方法。我知道你不能从 lambda 返回或破坏父函数,在比较它们时也应该考虑到这一点,但是还有什么需要考虑的吗?
joins.forEach((join) -> mIrc.join(mSession, join));
真的是 for (String join : joins) { mIrc.join(mSession, join); }
的“简化”吗?您已将标点符号数从 9 个增加到 12 个,以便隐藏 join
的类型。您真正所做的是将两个语句放在一行上。
更好的做法是使用 for-each
。除了违反Keep It Simple, Stupid 原则外,新奇的forEach()
至少还有以下不足:
不能使用非最终变量。因此,不能将如下代码转换为 forEach lambda:
对象上一个 = null; for(Object curr : list) { if( prev != null ) foo(prev, curr);上一页 = 当前; }
无法处理已检查的异常。 Lambdas 实际上并没有被禁止抛出检查异常,但是像 Consumer 这样的常见功能接口没有声明任何异常。因此,任何抛出检查异常的代码都必须将它们包装在 try-catch 或 Throwables.propagate() 中。但即使你这样做了,也并不总是清楚抛出的异常会发生什么。它可能会在 forEach() 的某个地方被吞下
有限的流量控制。 lambda 中的 return 等于 for-each 中的 continue,但不等同于 break。做返回值、短路或设置标志之类的事情也很困难(如果不违反无非最终变量规则,这会稍微缓解一些事情)。 “这不仅是一种优化,而且当您考虑到某些序列(例如读取文件中的行)可能有副作用,或者您可能有无限序列时,这一点至关重要。”
可能会并行执行,这对于除了需要优化的 0.1% 的代码之外的所有人来说都是一件可怕的事情。任何并行代码都必须经过深思熟虑(即使它不使用锁、易失性和传统多线程执行的其他特别讨厌的方面)。任何错误都很难找到。
可能会损害性能,因为 JIT 无法将 forEach()+lambda 优化到与普通循环相同的程度,尤其是现在 lambda 是新的。我所说的“优化”并不是指调用 lambdas 的开销(很小),而是指现代 JIT 编译器对运行代码执行的复杂分析和转换。
如果您确实需要并行性,那么使用 ExecutorService 可能会快得多,而且难度也不大。流既是自动的(阅读:对您的问题了解不多),又使用专门的(阅读:一般情况下效率低下)并行化策略(fork-join 递归分解)。
由于嵌套的调用层次结构和(上帝禁止)并行执行,使调试更加混乱。调试器在显示周围代码中的变量时可能会出现问题,并且诸如单步执行之类的操作可能无法按预期工作。
通常,流更难编码、读取和调试。实际上,一般来说,复杂的“流利”API 都是如此。复杂的单个语句、大量使用泛型以及缺少中间变量的组合共同产生了令人困惑的错误消息并阻碍了调试。而不是“此方法没有类型 X 的重载”,而是更接近于“您在某处弄乱了类型,但我们不知道在哪里或如何”的错误消息。同样,您不能像将代码分解为多个语句并将中间值保存到变量时那样在调试器中单步执行和检查事物。最后,阅读代码并了解每个执行阶段的类型和行为可能并非易事。
像拇指酸痛一样伸出来。 Java 语言已经有了 for-each 语句。为什么用函数调用代替它?为什么要鼓励在表达式的某处隐藏副作用?为什么要鼓励笨拙的单行字?将常规的 for-each 和新的 forEach 混用是不好的风格。代码应该用惯用语说话(由于重复而容易理解的模式),使用的惯用语越少,代码就越清晰,决定使用哪个惯用语的时间就越少(对于像我这样的完美主义者来说,这是一个很大的时间消耗! )。
如您所见,我不是 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))更干净地调用单个函数。但是,请记住检查异常、更难调试以及减少编写代码时使用的惯用语的数量。
我用来参考的文章:
关于 Java 8 的一切
内外迭代(正如另一张海报所指出的)
编辑: 看起来 lambdas 的一些原始提案(例如 http://www.javac.info/closures-v06a.html Google Cache)解决了我提到的一些问题(当然也增加了它们自己的复杂性)。
当操作可以并行执行时,优势就会被考虑在内。 (参见 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
接口,则不再适用。
map
& 等批量收集方法中。 fold
与 lambda 无关。
Stream#forEach
和 Iterable#forEach
不是一回事。 OP 在询问 Iterable#forEach
。
joins
正在实施 Iterable
而不是 Stream
,谁能向我解释为什么这个答案无效?从我读过的几件事来看,如果 joins
实现 Iterable
,OP 应该能够执行 joins.stream().forEach((join) -> mIrc.join(mSession, join));
和 joins.parallelStream().forEach((join) -> mIrc.join(mSession, join));
阅读此问题时,您会得到这样的印象,即 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 变量并不是函数/lambda 的缺陷,它是一个 功能,它将 Iterable#forEach
的语义与传统 for-each 循环的语义区分开来.一旦习惯了 Iterable#forEach
的语法,它就会使代码更具可读性,因为您会立即获得有关代码的附加信息。
传统的 for-each 循环肯定会在 Java 中保持良好做法(以避免过度使用术语“best practice”)。但这并不意味着,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
-client 的结果
-server 的结果
为并行执行提供可选支持:这里已经说过,使用流并行执行 Iterable#forEach 的功能接口的可能性当然是一个重要方面。由于 Collection#parallelStream() 不能保证循环实际上是并行执行的,因此必须将其视为一项可选功能。通过使用 list.parallelStream().forEach(...); 迭代您的列表,您明确地说:此循环支持并行执行,但它不依赖于它。同样,这是一个特点,而不是一个缺陷!通过将并行执行的决策从实际的循环实现中移开,您可以对代码进行可选优化,而不会影响代码本身,这是一件好事。此外,如果默认的并行流实现不能满足您的需求,没有人会阻止您提供自己的实现。例如,您可以根据底层操作系统、集合大小、内核数量和一些首选项设置提供优化的集合: public abstract class MyOptimizedCollection
forEach
和 for-each
之间切换。遵循这些规则的智慧和纪律是优秀程序员的标志。这样的规则也是他的祸根,因为他周围的人要么不遵守,要么不同意。例如,使用已检查与未检查的异常。这种情况似乎更加微妙。但是,如果主体“不影响环绕代码或流控制”,是不是将其作为一个函数分解出来更好?
But, if the body "does not affect surround code or flow control," isn't factoring it out as a function better?
。是的,在我看来,这通常是这种情况——将这些循环分解为函数是很自然的结果。
Iterable#forEach
的函数样式循环,只是因为性能提高。有问题的项目有一个类似于游戏循环的主循环,具有未定义数量的嵌套子循环,客户端可以将循环参与者作为函数插入。这样的软件结构极大地受益于Iteable#forEach
。
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/,以比较不同用例的内部/外部迭代。
void forEach(Consumer<T> v){leftTree.forEach(v);v.accept(rootElem);rightTree.forEach(v);}
,这比外部迭代更优雅,您可以决定如何最好地同步
String.join
方法中的唯一注释(好的,错误的连接)是“不太可能值得 Arrays.stream 开销的元素数量”。所以他们使用豪华的 for 循环。
TL;DR:List.stream().forEach()
是最快的。
我觉得我应该添加基准测试迭代的结果。我采用了一种非常简单的方法(没有基准测试框架)并对 5 种不同的方法进行了基准测试:
经典 foreach 的经典 List.forEach() List.stream().forEach() 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(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 毫秒
System.out.println
天真地显示此数据,那么所有结果都是无用的。
System.nanoTime()
。如果您阅读答案,您会看到它是如何完成的。我不认为这使它毫无用处,因为这是一个 relative 问题。我不在乎某种方法做得如何,我在乎它与其他方法相比做得如何。
我觉得我需要扩展我的评论......
关于范式\风格
这可能是最值得注意的方面。 FP 之所以流行,是因为您可以避免副作用。我不会深入研究您可以从中获得什么利弊,因为这与问题无关。
但是,我会说使用 Iterable.forEach 的迭代受到 FP 的启发,而是将更多 FP 引入 Java 的结果(具有讽刺意味的是,我会说 forEach 在纯 FP 中没有太多用处,因为它除了引入副作用)。
最后,我想说的是,您当前所写的内容与品味\风格\范式有关。
关于并行。
从性能的角度来看,使用 Iterable.forEach 而不是 foreach(...) 并没有显着的好处。
对 Iterable 的内容执行给定的操作,按照迭代时元素发生的顺序,直到所有元素都被处理或操作引发异常。
...即文档非常清楚不会有隐含的并行性。添加一个将违反 LSP。
现在,Java 8 中承诺了“并行集合”,但要与那些你需要我更明确地使用它们并在使用它们时格外小心(例如,参见 mschenk74 的答案)。
顺便说一句:在这种情况下 Stream.forEach 将被使用,它不保证实际工作将并行完成(取决于底层集合)。
更新:可能不是那么明显并且一目了然,但还有另一个方面的风格和可读性。
首先 - 普通的旧 forloops 是普通的和旧的。每个人都已经认识他们了。
其次,更重要的是 - 您可能只想将 Iterable.forEach 与单行 lambda 一起使用。如果“body”变得更重——它们往往不那么可读。从这里您有 2 个选项 - 使用内部类 (yuck) 或使用普通的旧 forloop。当人们看到在同一个代码库中以不同的方式/样式完成相同的事情(迭代集合)时,人们经常会感到恼火,这似乎就是这种情况。
同样,这可能是也可能不是问题。取决于从事代码工作的人。
collection.forEach(MyClass::loopBody);
最令人愉快的功能 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?
Java 1.8 forEach 方法优于 1.7 增强 for 循环的优势在于,在编写代码时,您可以只专注于业务逻辑。
forEach 方法将 java.util.function.Consumer 对象作为参数,因此它有助于将我们的业务逻辑放在一个单独的位置,您可以随时重用它。
看看下面的片段,
在这里,我创建了新的类,它将覆盖消费者类的接受类方法,您可以在其中添加额外的功能,不仅仅是迭代..!!!!!! class MyConsumer implements Consumer
forEach
用于鼓励函数式风格,即使用表达式没有副作用。如果您遇到forEach
无法很好地处理您的副作用的情况,您应该感觉到您没有使用正确的工具来完成这项工作。那么简单的答案是,那是因为您的感觉是正确的,所以请留在 for-each 循环中。经典的for
循环并没有被弃用……forEach
如何在没有副作用的情况下使用?forEach
是唯一用于副作用的流操作,但它不像您的示例代码那样用于副作用,计数是典型的reduce
操作。作为重击规则,我建议将每个操作局部变量或将影响控制流(包括异常处理)的操作保留在经典的for
循环中。关于我认为的原始问题,问题源于有人使用流,其中流源上的简单for
循环就足够了。使用仅适用于forEach()
的流forEach
适用的副作用示例是什么?for
循环。