Stream如何提高效率?

时间:2016-05-11 12:25:23

标签: java java-8 java-stream

请通过评论您对这个问题的投票来帮忙。

所以,首先请原谅,如果这看起来太天真了。我正在尝试消化Stream包,似乎我很难理解。

我正在阅读Stream软件包文档,在某一点上,我试图通过实践来实现它。这是我读过的文字:

  

中间操作返回一个新流。他们总是懒惰;   执行诸如filter()之类的中间操作实际上并不是这样   执行任何过滤,而是创建一个新的流,当时   遍历,包含匹配的初始流的元素   给出谓词。直到管道源的遍历才开始   执行管道的终端操作。

我非常了解他们提供了一个新的Stream,所以我的第一个问题是,创建一个流而不会遍历繁重的操作吗?

现在,由于中间操作为lazy,终端操作为eager,因此流的效率要高于if-else的旧编程标准且更具可读性。

  

懒洋洋地处理流可以显着提高效率;在一个   管道,如上面的filter-map-sum示例,过滤,映射,   和求和可以融合到数据的单个传递中,最小化   中间状态。懒惰还允许避免检查所有   没有必要时的数据;对于诸如“找到第一个”之类的操作   字符串超过1000个字符“,只需要检查   只需足够的字符串即可找到具有所需特征的字符串   没有检查源中可用的所有字符串。 (这个   当输入流是无限的时,行为变得更加重要   而不仅仅是大的。)

为了证明这一点,我开始实施一个小程序来理解这个概念。这是程序:

        List<String> stringList = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            stringList.add("String" + i);
        }
        long   start  = System.currentTimeMillis();
        Stream stream = stringList.stream().filter(s -> s.contains("99"));
        long   midEnd = System.currentTimeMillis();
        System.out.println("Time is millis before applying terminal operation: " + (midEnd - start));
        System.out.println(stream.findFirst().get());
        long end = System.currentTimeMillis();
        System.out.println("Whole time in millis: " + (end - start));
        System.out.println("Time in millis for Terminal operation: " + (end - midEnd));

        start = System.currentTimeMillis();
        for (String ss1 : stringList) {
            if (ss1.contains("99")) {
                System.out.println(ss1);
                break;
            }
        }
        end = System.currentTimeMillis();
        System.out.println("Time in millis with old standard: " + (end - start));

我已多次执行此程序,每次它都证明了这一点,从中间操作创建新流是一项繁重的任务。与中间操作相比,终端操作确实花费很少的时间。

总体而言,旧的if-else模式比streams更有效。所以,这里还有更多问题:

  1. 我误解了什么吗?
  2. 如果我理解正确,为什么以及何时使用流?
  3. 如果我正在做或理解任何错误,请帮助澄清我的概念Package java.util.stream
  4. 实际数字:

    尝试1:

    Time is millis before applying terminal operation: 73
    String99
    Whole time in millis: 76
    Time in millis for Terminal operation: 3
    String99
    Time in millis with old standard: 0
    

    尝试2:

    Time is millis before applying terminal operation: 56
    String99
    Whole time in millis: 59
    Time in millis for Terminal operation: 3
    String99
    Time in millis with old standard: 0
    

    尝试3:

    Time is millis before applying terminal operation: 69
    String99
    Whole time in millis: 72
    Time in millis for Terminal operation: 3
    String99
    Time in millis with old standard: 0
    

    如果有这方面的帮助,这些是我的机器详细信息:

    Memory: 11.6 GiB
    Processor: Intel® Core™ i7-3632QM CPU @ 2.20GHz × 8 
    OS-Type: 64-bit
    

    注意:我无法找到答案,所以如果它看似重复,请这样做。

4 个答案:

答案 0 :(得分:4)

Stream api的一个基本原理是它消除了for循环的固有假设,即所有迭代都以相同的方式发生。当您使用基于迭代器的for循环时,您正在对迭代逻辑进行硬编码以始终按顺序迭代。考虑一下这个问题,“如果我想用更高效的东西来改变'for'循环的实现怎么办?”

Stream api解决了这个问题 - 它抽象了迭代的概念,并允许考虑处理多个数据点的其他方法 - 串行和并行迭代,如果已知数据是无序的,则添加优化等等

考虑一下您的示例 - 尽管您无法更改for循环的实现,但您可以更改Stream的实现以适应不同的情况。例如,如果要对每项任务执行更多cpu密集型操作,则可以选择并行Stream。这是一个10 ms延迟模拟更复杂处理的示例,并行完成,结果非常不同:

    List<String> stringList = new ArrayList<>();
    for (int i = 0; i < 10000; i++) {
        stringList.add("String" + i);
    }
    long   start  = System.currentTimeMillis();
    Stream stream = stringList.parallelStream().filter(s -> {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return s.contains("99" );});
    long   midEnd = System.currentTimeMillis();
    System.out.println("Time is millis before applying terminal operation: " + (midEnd - start));
    System.out.println(stream.findAny().get());
    long end = System.currentTimeMillis();
    System.out.println("Whole time in millis: " + (end - start));
    System.out.println("Time in millis for Terminal operation: " + (end - midEnd));

    start = System.currentTimeMillis();
    for (String ss1 : stringList) {
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (ss1.contains("99")) {
            System.out.println(ss1);
            break;
        }
    }
    end = System.currentTimeMillis();
    System.out.println("Time in millis with old standard: " + (end - start));

我保持每个人都抱怨的基准逻辑,让您更容易比较。

正如您所看到的,有些情况for循环总是比使用Stream更有效,但Streams在某些情况下也提供了显着的优势。从一个孤立的测试中推断出一种方法总是优于另一种方法是不明智的 - 这也是生命的公理。

答案 1 :(得分:3)

除非你的测试涉及JMH,否则你的代码几乎就是一无所有,更糟糕的是,它会给出改变现实的印象

assylias发表了评论,应该明确哪些方面出了问题。

您对&#34;中间操作的测量&#34>然后是&#34;短路&#34;也错了。中间操作,因为它是懒惰的,什么都不做,它只会在终端进入时才会发生。

如果你曾经使用过番石榴,那么转换/过滤器​​的代码也是如此,至少在逻辑上如此。

答案 2 :(得分:2)

我真的不相信你的“基准”,因为can go wrong太多了,你最好使用framework。但无论如何,当人们或文档说它更有效时,它们并不代表你提供的例子。

作为提升集合的流(它们不保存数据)比Scala Lists等热切的集合更有效,例如filter分配新列表和{{1将结果转换为新的map

当我们与此实现List获胜时进行比较。 但是,对于现代Streams并在现代JVMs中照顾,所以流分配对象是廉价的。

答案 3 :(得分:2)

正如其他人已经注意到你的基准是有缺陷的。主要问题是忽略编译时间会导致结果偏差。请尝试以下方法:

    Stream stream = stringList.stream().filter(s -> s.contains("99"));
    long   start  = System.currentTimeMillis();
    stream = stringList.stream().filter(s -> s.contains("99"));
    long   midEnd = System.currentTimeMillis();

现在支持filter的代码已经编译,第二次调用很快。即使这样也可行:

    Stream stream = stringList.stream().map(s -> s);
    long   start  = System.currentTimeMillis();
    stream = stringList.stream().filter(s -> s.contains("99"));
    long   midEnd = System.currentTimeMillis();

mapfilter共享大部分代码,因此调用filter也很快,因为代码已经编译完毕。如果你问:在不同的流上拨打filtermap当然也会有效。

您的“旧样式”代码不需要额外编译。