java8流是否会产生慢速代码?

时间:2018-01-14 11:00:02

标签: performance java-8 functional-programming java-stream

关于函数式编程,特别是新的Java 8流API,有太多的炒作。它被宣传为旧的良好循环和命令式范例的良好替代品。 事实上,有时它看起来很不错并且做得很好。但是性能呢?

E.g。这是关于此的好文章:Java 8: No more loops 使用循环,您可以通过一次迭代完成所有工作。但是使用新的流API,您将链接多个循环,这使得它更慢(是不是?)。 看看他们的第一个样本。在大多数情况下,Loop甚至不会遍历整个阵列。但是,要使用新的流API进行过滤,您必须循环遍历整个数组以过滤掉所有候选项,然后您将能够获得第一个。

在这篇文章中提到了一些懒惰:

  

我们首先使用过滤器操作来查找具有Java标记的所有文章,然后使用findFirst()操作来获取第一个匹配项。由于流是惰性的并且过滤器返回流,因此该方法仅处理元素,直到找到第一个匹配。

作者对这种懒惰的意思是什么?

我做了简单的测试,它表明旧的良好循环解决方案可以快速运行10倍然后流式处理。

public void test() {
    List<String> list = Arrays.asList(
            "First string",
            "Second string",
            "Third string",
            "Good string",
            "Another",
            "Best",
            "Super string",
            "Light",
            "Better",
            "For string",
            "Not string",
            "Great",
            "Super change",
            "Very nice",
            "Super cool",
            "Nice",
            "Very good",
            "Not yet string",
            "Let's do the string",
            "First string",
            "Low string",
            "Big bunny",
            "Superstar",
            "Last");

    long start = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        getFirstByLoop(list);
    }
    long end = System.currentTimeMillis();

    System.out.println("Loop: " + (end - start));

    start = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        getFirstByStream(list);
    }
    end = System.currentTimeMillis();

    System.out.println("Stream: " + (end - start));
}

public String getFirstByLoop(List<String> list) {

    for (String s : list) {
        if (s.endsWith("string")) {
            return s;
        }
    }

    return null;
}

public Optional<String> getFirstByStream(List<String> list) {
    return list.stream().filter(s -> s.endsWith("string")).findFirst();
}

结果是:

循环:517

Stream:5790

顺便说一句,如果我使用String []而不是List,差异会更大!差不多100倍!

问题:如果我正在寻找最佳的代码性能,我应该使用旧循环命令式方法吗? FP范式只是为了使代码更简洁,更易读和可读性。但不是关于表现?

OR

有什么我错过了,新的流API可能至少与循环命令式方法一样有效吗?

2 个答案:

答案 0 :(得分:8)

懒惰是关于如何从流源获取元素 - 即按需 。如果需要采取更多的元素 - 他们会,否则他们不会。这是一个例子:

 Arrays.asList(1, 2, 3, 4, 5)
            .stream()
            .peek(x -> System.out.println("before filter : " + x))
            .filter(x -> x > 2)
            .peek(System.out::println)
            .anyMatch(x -> x > 3);

注意每个元素如何遍历整个阶段的管道; filter是时候应用于一个元素 - 而不是所有元素,因此filter会返回Stream<Integer>。这允许流短路,因为anyMatch甚至不处理5,因为根本不需要。

请注意,并非所有中间操作都是懒惰的。例如sorteddistinct不是 - 这些被称为有状态中间操作。想一想 - 实际排序所需的元素需要遍历整个源。另一个不直观的例子是flatMap,但这不能得到保证,看起来更像是一个错误,更多的是阅读here

速度与测量方法有关,在java中测量微基准并不容易,事实上的工具是jmh - 你可以尝试一下。 SO上有很多帖子表明流确实比较慢(这在正常情况下 - 它们有一个基础设施),但实际关心的差别并不大。

答案 1 :(得分:6)

  

问题:如果我正在寻找最佳的代码性能,我应该使用旧的循环命令式方法吗?

现在,可能是的。各种基准测试似乎表明,对于大多数测试,流比循环慢。虽然没有灾难性的慢。

反例:

可以使用循环执行相同的操作,但不能使用 循环执行此操作。

但最重要的是,性能很复杂,而且流还不是加速代码的神奇子弹。

  

FP范式只是为了使代码“更简洁和可读”而不是性能吗?

不完全是。毫无疑问,FP范式更简洁,并且(对熟悉它的人)更具可读性。

然而,通过表达使用FP范例,您以某种方式表达它,这种方式可能通过使用循环和赋值表示的代码更难以实现的方式进行优化。 FP代码也更适合正式方法;即正确的正式证明。