IntelliJ IDEA 建议用 foreach 方法替换 for 循环。如果可能的话,我应该总是这样做吗?

2022-09-04 20:06:36

IDEA建议替换,例如,这个:

for (Point2D vertex : graph.vertexSet()) {
  union.addVertex(vertex);
}

有了这个:

graph.vertexSet().forEach(union::addVertex);

这个新版本肯定更具可读性。但是,在某些情况下,我最好坚持使用良好的旧语言结构来迭代,而不是使用新方法吗?foreach

例如,如果我理解正确,方法引用机制意味着构造一个匿名对象,否则(使用语言构造)将无法构造。这是否会成为某些操作的性能瓶颈?Consumerfor

所以我写了这个不是很详尽的基准测试:

package org.sample;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.infra.Blackhole;
import org.tendiwa.geometry.Point2D;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class LanguageConstructVsForeach {
    private static final int NUMBER_OF_POINTS = 10000;
    private static final List<Point2D> points = IntStream
        .range(0, NUMBER_OF_POINTS)
        .mapToObj(i -> new Point2D(i, i * 2))
        .collect(Collectors.toList());

    @Benchmark
    @Threads(1)
    @Fork(3)
    public void languageConstructToBlackhole(Blackhole bh) {
        for (Point2D point : points) {
            bh.consume(point);
        }
    }
    @Benchmark
    @Threads(1)
    @Fork(3)
    public void foreachToBlackhole(Blackhole bh) {
        points.forEach(bh::consume);
    }
    @Benchmark
    @Threads(1)
    @Fork(3)
    public List<Point2D> languageConstructToList(Blackhole bh) {
        List<Point2D> list = new ArrayList<>(NUMBER_OF_POINTS);
        for (Point2D point : points) {
            list.add(point);
        }
        return list;
    }
    @Benchmark
    @Threads(1)
    @Fork(3)
    public List<Point2D> foreachToList(Blackhole bh) {
        List<Point2D> list = new ArrayList<>(NUMBER_OF_POINTS);
        points.forEach(list::add);
        return list;
    }

}

并得到:

Benchmark                                                       Mode  Samples      Score     Error  Units
o.s.LanguageConstructVsForeach.foreachToBlackhole              thrpt       60  33693.834 ± 894.138  ops/s
o.s.LanguageConstructVsForeach.foreachToList                   thrpt       60   7753.941 ± 239.081  ops/s
o.s.LanguageConstructVsForeach.languageConstructToBlackhole    thrpt       60  16043.548 ± 644.432  ops/s
o.s.LanguageConstructVsForeach.languageConstructToList         thrpt       60   6499.527 ± 202.589  ops/s

在这两种情况下,为什么更有效率:当我几乎什么都不做和做一些实际工作时?不是简单地封装?这个基准测试是否正确?如果是这样,今天有什么理由在Java 8中使用旧语言结构吗?foreachforeachIterator


答案 1

您正在将该语言的“enhanced-for”循环与 Iterable.forEach() 方法进行比较。基准测试并没有明显的错误,结果可能看起来令人惊讶,直到你深入研究实现。

请注意,该列表是的一个实例,因为这是收集器创建的内容。pointsArrayListCollectors.toList()

上的增强 for 循环从中获取 a,然后调用并重复调用,直到没有更多元素。(这与数组上的增强型 for 循环不同,后者执行算术和直接数组元素访问。因此,当循环通过 时,此循环每次迭代将执行至少两个方法调用。IterableIteratorhasNext()next()Iterable

相比之下,调用在包含列表元素的数组上运行传统的、基于 int 的 for 循环,并且每次迭代调用 lambda 一次。此处每次迭代只有一个调用,而增强型 for 循环每次迭代只有两个调用。这也许可以解释为什么在这种情况下更快。ArrayList.forEach()ArrayList.forEach()

黑洞情况似乎除了运行循环之外几乎没有做任何工作,因此这些情况似乎正在测量纯循环开销。这可能就是为什么在这里显示出如此大的优势。ArrayList.forEach()

当循环只做一点点工作时(添加到目标列表),仍然有 速度优势,但差异要小得多。我怀疑,如果你在循环中做更多的工作,优势会更小。这表明任何一种构造的循环开销都非常小。尝试在循环中使用。如果两个结构之间的结果变得难以区分,我不会感到惊讶。ArrayList.forEach()BlackHole.consumeCPU()

请注意,之所以出现巨大的速度优势,是因为最终在 中具有专门的实现。如果您要运行不同的数据结构,则可能会得到不同的结果。Iterable.forEach()ArrayList.forEach()forEach()

我不会以此为理由盲目地将所有增强型循环替换为对 .编写最清晰、最有意义的代码。如果您正在编写性能关键型代码,请对其进行基准测试!不同的表单将具有不同的性能,具体取决于工作负载,正在遍历的数据结构等。Iterable.forEach()


答案 2

使用旧样式的一个明显原因是您将与Java 7兼容。如果你的代码使用了很多新奇的Java 8好东西,这不是一个选择,但如果你只使用一些新功能,这可能是一个优势,特别是如果你的代码在通用库中。