Java 8 Stream:limit() 和 skip() 之间的区别

2022-08-31 14:43:54

说到s,当我执行这段代码时Stream

public class Main {
    public static void main(String[] args) {
        Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .limit(3)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));
    }
}

我得到这个输出

A1B1C1
A2B2C2
A3B3C3

因为将我的流限制为前三个组件,强制操作 ABC 仅执行三次。

尝试使用方法对最后三个元素执行类似的计算,显示了不同的行为:skip()

public class Main {
    public static void main(String[] args) {
        Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .skip(6)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));
    }
}

输出此

A1
A2
A3
A4
A5
A6
A7B7C7
A8B8C8
A9B9C9

在这种情况下,为什么执行操作 A1A6?它必须与极限短路状态中间操作的事实有关,而跳过则不是,但我不明白这种性质的实际含义。只是“跳过之前的每个操作都被执行,而限制之前的每个人都是”吗?


答案 1

这里包含的是两个流管道。

这些流管道分别由一个源、多个中间操作和一个终端操作组成。

但中间操作是懒惰的。这意味着除非下游操作需要项目,否则不会发生任何操作。当它这样做时,中间操作将完成生成所需项所需的所有操作,然后再次等待,直到请求另一项,依此类推。

终端操作通常是“急切的”。也就是说,他们要求流中完成所需的所有项目。

因此,您应该真正将管道视为向其后面的流询问下一项,并且该流向其后面的流询问,依此类推,一直到源。forEach

考虑到这一点,让我们看看你的第一个管道有什么:

Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .limit(3)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));

因此,要求第一项。这意味着“B”需要一个项目,并为其请求输出流,这意味着需要询问“A”,该“A”将转到源。给出了一个项目,一直到 ,然后你得到你的第一行:forEachpeeklimitlimitpeekforEach

A1B1C1

要求另一个项目,然后是另一个项目。每次,请求都会沿流向上传播并执行。但是当要求第四个项目时,当请求到达时,它知道它已经给出了允许它给出的所有项目。forEachforEachlimit

因此,它不是要求“A”窥视另一个项目。它立即指示其项目已用尽,因此,不再执行任何操作并终止。forEach

在第二个管道中会发生什么?

    Stream.of(1,2,3,4,5,6,7,8,9)
    .peek(x->System.out.print("\nA"+x))
    .skip(6)
    .peek(x->System.out.print("B"+x))
    .forEach(x->System.out.print("C"+x));

再次,要求第一项。这被传播回去。但是当它到达时,它知道它必须从上游要求6个项目,然后才能通过下游的一个项目。因此,它从“A”上游发出请求,在不将其传递到下游的情况下使用它,然后发出另一个请求,依此类推。因此,“A”透视获得对一个项目的6个请求,并产生6个打印件,但这些项目不会传递下来。forEachskippeek

A1
A2
A3
A4
A5
A6

在 提出的第 7 个请求中,该项目被传递到 “B” peek 并从它传递到 ,因此完成完整打印:skipforEach

A7B7C7

然后就像以前一样。现在,每当收到请求时,它将向上游请求一个项目并将其传递到下游,因为它“知道”它已经完成了跳过工作。因此,其余的印刷品都经过整个管道,直到源头耗尽。skip


答案 2

流式管道的流畅表示法是导致这种混淆的原因。可以这样想:

limit(3)

所有流水线操作都懒惰地计算,除了 ,这是一个终端操作,它触发“流水线的执行”。forEach()

执行管道时,中间流定义不会对“之前”或“之后”发生的情况做出任何假设。他们所要做的就是获取输入流并将其转换为输出流:

Stream<Integer> s1 = Stream.of(1,2,3,4,5,6,7,8,9);
Stream<Integer> s2 = s1.peek(x->System.out.print("\nA"+x));
Stream<Integer> s3 = s2.limit(3);
Stream<Integer> s4 = s3.peek(x->System.out.print("B"+x));

s4.forEach(x->System.out.print("C"+x));
  • s1包含 9 个不同的值。Integer
  • s2查看传递它的所有值并打印它们。
  • s3将前 3 个值传递到,并在第三个值之后中止管道。不会生成其他值。这并不意味着管道中没有更多值。s2 仍将生成(和打印)更多值,但没有人请求这些值,因此执行停止。s4s3
  • s4再次查看传递它的所有值并打印它们。
  • forEach消耗并打印传递给它的任何内容。s4

这样想吧。整个流完全是懒惰的。只有终端操作会主动从管道中提取新值。从 中提取 3 个值后,将不再生成新值,也不再从 中提取任何值。虽然仍然能够产生,但这些值永远不会从管道中提取,因此永远不会被打印出来。s4 <- s3 <- s2 <- s1s3s2 <- s1s1 -> s24-9s2

skip(6)

同样的事情发生了:skip()

Stream<Integer> s1 = Stream.of(1,2,3,4,5,6,7,8,9);
Stream<Integer> s2 = s1.peek(x->System.out.print("\nA"+x));
Stream<Integer> s3 = s2.skip(6);
Stream<Integer> s4 = s3.peek(x->System.out.print("B"+x));

s4.forEach(x->System.out.print("C"+x));
  • s1包含 9 个不同的值。Integer
  • s2查看传递它的所有值并打印它们。
  • s3使用前 6 个值,“跳过它们”,这意味着前 6 个值不会传递到 ,只有后续值是。s4
  • s4再次查看传递它的所有值并打印它们。
  • forEach消耗并打印传递给它的任何内容。s4

这里重要的是,不知道剩余的管道跳过任何值。 独立于之后发生的事情窥视所有值。s2s2

另一个例子:

请考虑此管道,此博客文章中列出了此管道

IntStream.iterate(0, i -> ( i + 1 ) % 2)
         .distinct()
         .limit(10)
         .forEach(System.out::println);

当您执行上述操作时,程序将永远不会停止。为什么?因为:

IntStream i1 = IntStream.iterate(0, i -> ( i + 1 ) % 2);
IntStream i2 = i1.distinct();
IntStream i3 = i2.limit(10);

i3.forEach(System.out::println);

这意味着:

  • i1生成无限数量的交替值:、 、 、 、 ...010101
  • i2消耗以前遇到过的所有值,仅传递“新”值,即 总共有 2 个值来自 。i2
  • i3传递 10 个值,然后停止。

此算法永远不会停止,因为 等待 在 和 之后再生成 8 个值,但这些值永远不会出现,而永远不会停止将值馈送到 。i3i201i1i2

在管道中的某个时刻,已经产生了10多个值,这并不重要。重要的是从未见过这10个值。i3

要回答您的问题:

只是“跳过之前的每个操作都被执行,而限制之前的每个人都是”吗?

不。执行之前的所有操作或执行。在你的两次处决中,你都会得到 - .但可能会使管道短路,一旦发生感兴趣的事件(达到极限),就会中止值消耗。skip()limit()A1A3limit()


推荐