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

谈论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 

因为限制我的stream到前三个组件迫使行动ABC只能执行三次。

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

 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的动作? 它必须与限制是短路状态中间操作的事实有关,而跳过不是,但我不明白这个属性的实际含义。 只是“ 跳过之前的每个动作被执行,而不是每个人都在限制之前”?

你在这里有两个streampipe道。

这些streampipe道每个都由一个源,几个中间操作和一个terminal操作组成。

但是中间操作是懒惰的。 这意味着什么都不会发生,除非下游操作需要一个项目。 当它这样做时,中间操作完成所需的所有工作,然后再等待,直到另一个项目被请求,等等。

terminal操作通常是“热切”的。 也就是说,他们要求stream中所有需要完成的项目。

所以你应该真的把stream水线想象成forEach将后面的数据stream请求下一个数据stream,然后这个数据stream请求stream中的数据stream,以此类推,直到源数据。

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

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

所以, forEach是要求第一个项目。 这意味着“B” peek需要一个项目,并要求limit输出stream,这意味着limit将需要问“ peek ,这源于。 一个项目被给出,并一直到forEach ,你得到你的第一行:

 A1B1C1 

forEach要求另一个项目,然后另一个项目。 并且每次,请求被传播到stream中,并被执行。 但是当forEach要求第四项时,当请求达到limit ,它知道它已经给出了所有允许的项目。

因此,它不是要求“A”偷看另一个项目。 它立即表明它的物品已经耗尽,因此不再执行任何动作,并终止forEach动作。

第二条pipe线发生了什么?

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

再次, forEach要求第一个项目。 这是传播回来。 但是,当它skip下游时,它知道它必须从其上游请求6个物品,然后才能通过下游。 所以它在“A” peek上游提出请求,在不传递下游的情况下使用它,提出另一个请求,等等。 因此,“A”偷看获得一个项目的6个请求,并产生6打印,但这些项目不传递。

 A1 A2 A3 A4 A5 A6 

skip的第七个请求中,该项目被传递给“B”,并从它到forEach ,所以完整的打印完成:

 A7B7C7 

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

stream式pipe道的stream畅符号是造成这种混淆的原因。 这样想一想:

limit(3)

所有的stream水线操作都是懒惰的评估,除了terminal操作的触发器forEach() ,它触发“pipe道的执行”

当stream水线执行时,中介stream定义将不会对“之前”“之后”发生的情况作出任何假设。 他们所做的只是将inputstream转换为输出stream:

 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将前三个值传递给s4并在第三个值后中止pipe道。 s3没有产生更多的值。 这并不意味着没有更多的价值在pipe道中。 s2仍然会产生(和打印)更多的值,但没有人请求这些值,从而停止执行。
  • s4再次偷看所有通过它的值并打印出来。
  • forEach消费和打印任何s4传递给它。

想想这样。 整个stream是完全懒惰的。 只有terminal操作主动从pipe道中拉出新的值。 从s4 <- s3 <- s2 <- s1拉出3个数值后, s3将不再产生新的数值,并且不再从s2 <- s1取任何数值。 虽然s1 -> s2仍然能够产生4-9 ,但这些值只是从来没有从pipe道中拉出,因此从不打印s2

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传递给它。

这里重要的是s2没有意识到剩余的pipe道跳过任何值。 s2窥视所有的价值,而不pipe事后会发生什么。

另一个例子:

考虑这个pipe道, 这是在这篇博客文章中列出

 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生成无限量的交替值: i1 ,…
  • i2消耗之前遇到的所有值,仅传递“新”值,即总共有2个值来自i2
  • i3传递10个值,然后停止。

这个algorithm将永远不会停止,因为i301之后等待i2产生8个更多的值,但是这些值从不出现,而i1从不停止向i2馈送值。

在pipe道的某一点上,已经产生了10多个值并不重要。 重要的是i3从来没有见过这10个值。

回答你的问题:

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

不。 执行skip()limit()之前的所有操作。 在你的执行中,你会得到A1A3 。 但是limit()可能会使pipe道短路,一旦发生感兴趣事件(达到限制),就会中止消耗。

单独考察蒸汽运行是完全亵渎的,因为这不是如何评估蒸汽。

谈到极限(3) ,这是一个短路操作,因为考虑它,无论是 limit 之前还是之后的操作都是有意义的,在stream中有一个极限将在获得n个元素之后停止迭代直到极限操作,但是这并不意味着只有n个stream元素会被处理。 以这个不同的stream操作为例

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

会输出

 A1 A2B2C2 A3 A4B4C4 A5 A6B6C6 

这似乎是正确的,因为限制正在等待3个stream元素通过操作链,尽pipe6个stream元素被处理。

所有stream都基于分割器,基本上有两个操作:advance(向前移动一个元素,类似于迭代器)和split(自我分割为任意位置,适合并行处理)。 您可以随时停止input元素(这是通过limit来完成的),但不能跳到任意位置(在Spliterator界面中没有这样的操作)。 因此, skip操作需要实际读取来源的第一个元素,而忽略它们。 请注意,在某些情况下,您可以执行实际跳转:

 List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9); list.stream().skip(3)... // will read 1,2,3, but ignore them list.subList(3, list.size()).stream()... // will actually jump over the first three elements