Java 8 Iterable.forEach() vs foreach循环

以下哪一项在Java 8中是更好的实践?

Java 8:

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

Java 7:

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

我有很多for循环可以用lambdas来“简化”,但是使用它们真的有任何优势吗?它会提高性能和可读性吗?

编辑

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

474843 次浏览

当操作可以并行执行时,优势就显现出来了。(参见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接口,则不再是这样。

我觉得我有必要扩展一下我的评论……

关于范式\风格

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

不过,我要说的是使用Iterable的迭代。forEach的灵感来自FP,是将更多的FP引入Java的结果(具有讽刺意味的是,我要说forEach在纯FP中没有多大用处,因为它除了引入副作用之外什么都不做)。

最后,我想说的是,这是你目前写作的品味、风格和范式的问题。

并行性。

从性能的角度来看,使用Iterable并没有明显的好处。一个接一个(…)。

根据官方Iterable.forEach上的docs:

中的,则对Iterable的内容执行给定的操作 Order元素在迭代时出现,直到所有元素都被

.

.

.

... 例如,文档非常清楚,将没有隐式并行。增加一个将违反LSP。

现在,Java 8中承诺了“并行集合”,但要使用这些集合,你需要更加明确,并在使用它们时多加注意(例如,请参阅mschenk74的回答)。

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

更新:可能不是那么明显,乍一看有点拉长,但从风格和可读性的角度来看,还有另一个方面。

首先,简单的forloop是简单而古老的。大家都已经知道了。

第二,也是更重要的——你可能想要使用Iterable。forEach只使用一行程序lambdas。如果“身体”变重了,它们就不那么可读了。 从这里你有两个选择-使用内部类(讨厌)或使用普通的旧forloop。 当人们看到相同的事情(集合上的迭代)在相同的代码库中执行不同的vay /风格时,他们经常会感到恼火,这似乎是事实

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

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

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

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

与for-each循环相反,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/比较不同用例的内部/外部迭代。

更好的做法是使用for-each。除了违反保持简单,傻瓜原则外,新出现的forEach()至少存在以下不足:

  • # EYZ0。因此,像下面这样的代码不能转换为forEach lambda:
Object prev = null;
for(Object curr : list)
{
if( prev != null )
foo(prev, curr);
prev = curr;
}
  • < p > # EYZ4。Lambdas实际上并没有被禁止抛出受控异常,但是像Consumer这样的常见函数接口并没有声明任何异常。因此,任何抛出检查异常的代码都必须将它们包装在try-catchThrowables.propagate()中。但即使这样做了,也并不总是清楚抛出的异常会发生什么。它可能会被EYZ3吞噬

  • < p > # EYZ3。lambda中的return等于for-each中的continue,但不等于break。执行返回值、短路或设置标志之类的操作也很困难(如果不违反没有非最终变量规则,这本来可以缓解一些问题)。# EYZ6

  • 可以并行执行,这对所有人来说都是非常非常糟糕的事情,只有0.1%的代码需要优化。任何并行代码都必须仔细考虑(即使它不使用锁、volatile和其他传统多线程执行中特别讨厌的方面)。任何漏洞都很难找到。

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

  • < p > # EYZ0。流都是自动的(读:不太了解你的问题)而且使用了专门的(读:对于一般情况来说效率很低)并行化策略(Fork-join递归分解)。

  • 使调试更加混乱,因为嵌套的调用层次结构和并行执行。调试器可能会在显示周围代码中的变量时出现问题,并且像逐步执行这样的事情可能无法按预期工作。

  • < p > # EYZ0。事实上,对于复杂的"流利的"一般的api。复杂的单个语句、泛型的大量使用以及中间变量的缺乏,这些组合在一起会产生令人困惑的错误消息并阻碍调试。与“;”不同的是,这个方法对x类型没有重载;你得到一个错误消息,接近“某处你搞砸了类型,但我们不知道在哪里或如何。”类似地,当代码被分解成多条语句,中间值被保存到变量中时,您不能在调试器中轻松地逐级检查内容。最后,阅读代码并理解执行的每个阶段的类型和行为可能不是一件简单的事情。

  • < p > # EYZ0。Java语言已经有了for-each语句。为什么用函数调用替换它?为什么鼓励在表达式中隐藏副作用呢?为什么要鼓励笨拙的一行程序?随意地把常规和新forEach混在一起是不好的风格。代码应该使用习惯用语(由于重复而容易理解的模式),使用的习惯用语越少,代码就越清晰,花在决定使用哪种习惯用语上的时间就越少(对于像我这样的完美主义者来说,这是一笔很大的时间消耗!)

如您所见,我不是forEach()的忠实粉丝,除非在它有意义的情况下。

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

也就是说,forEach()在以下方面是有用的:

  • < p > # EYZ1。在此之前,使用Collections.synchronizedList()生成的列表对于get或set之类的东西是原子的,但在迭代时不是线程安全的。

  • < p > # EYZ0。如果您的问题与Streams和spliterator中构建的性能假设相匹配,那么与使用ExecutorService相比,这可以节省几行代码。

  • 特定的容器,像同步列表一样,受益于对迭代的控制(尽管这在很大程度上是理论的,除非人们能拿出更多的例子)

  • 更清晰地调用单个函数通过使用forEach()和方法引用参数(即list.forEach (obj::someMethod))。但是,请记住关于受控异常、更困难的调试以及在编写代码时减少使用的习惯用语的要点。

我参考的文章:

看起来lambdas的一些原始建议(如http://www.javac.info/closures-v06a.html 谷歌缓存)解决了我提到的一些问题(当然,同时增加了它们自己的复杂性)。

当阅读这个问题时,人们可能会有这样的印象,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的缺陷,而是功能Iterable#forEach的语义与传统for-each循环的语义区别开来。一旦习惯了Iterable#forEach的语法,就会使代码更具可读性,因为您可以立即获得关于代码的附加信息。

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

因为Iterable#forEach的缺点已经在这篇文章中讨论过了,这里有一些原因,为什么你可能想要使用Iterable#forEach:

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

  • 使用一个函数作为循环的主体允许你用不同的实现替换这个函数(见策略模式)。例如,你可以很容易地用一个方法调用替换lambda表达式,这个方法调用可以被子类覆盖:

    joins.forEach(getJoinStrategy());
    

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

  • 将循环实现与声明分离也可以使调试更容易,因为你可以有一个专门的调试实现,它打印出调试消息,而不需要用if(DEBUG)System.out.println()使你的主代码变得混乱。调试实现可以是一个委托,即装饰实际的函数实现。

  • 与这个线程中的一些断言相反,Iterable#forEach 已经提供了比传统的for-each循环更好的性能,至少在使用ArrayList和在“-客户端”模式下运行Hotspot时是这样。虽然这种性能提升对于大多数用例来说很小,可以忽略不计,但在某些情况下,这种额外的性能可能会产生影响。例如,库维护者肯定会想要评估,他们现有的一些循环实现是否应该替换为Iterable#forEach

    为了用事实支持这一说法,我用卡尺做了一些微型基准测试。下面是测试代码(需要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循环,但仍然比直接遍历数组要慢。当使用"-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();
    }
    }
    }
    

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

博士TL;: List.stream().forEach()最快。

我觉得我应该添加我的基准测试迭代的结果。 我采用了一种非常简单的方法(没有基准测试框架),并对5种不同的方法进行了基准测试
  1. 经典# EYZ0
  2. 经典的foreach
  3. # EYZ0
  4. # EYZ0
  5. # EYZ0

测试程序和参数

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,这就是结果,平均时间。 例子:< / p >
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

经典# EYZ0

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毫秒

# EYZ0

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

执行时间:3.11 ms

# EYZ0

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

执行时间:2.79 ms

# EYZ0

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

执行时间:3.6 ms

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

以下是lambdas和流中检查异常处理的其他变通方法中最常见的问题:

Java 8 Lambda函数抛出异常?< / >

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

我如何从Java 8流内部抛出CHECKED异常?< / >

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

Java 1.8 forEach方法相对于1.7 Enhanced for循环的优点是,在编写代码时,您可以只关注业务逻辑。

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

请看下面的片段,

    这里我创建了一个新类,它将覆盖消费者类的接受类方法, 在那里你可以添加额外的功能,不仅仅是迭代..!!!!!!< / p >
    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);
    }
    });
    }
    }