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

时间:2015-09-05 14:17:06

标签: java java-8 limit java-stream 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

因为将我的流限制为前三个组件会强制执行操作 A B C 只能执行三次。

尝试使用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

为什么在这种情况下,正在执行 A1 A6 的操作?它必须与 limit short-circuiting stateful intermediate operation这一事实有关,而 skip 不是,但我不明白这个属性的实际含义。只是“在跳过之前的所有操作都被执行而不是每个人都在限制之前”?

5 个答案:

答案 0 :(得分:92)

这里有两个流管道。

这些流管道每个都包含一个源,几个中间操作和一个终端操作。

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

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

因此,您应该将管道视为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));

所以,forEach要求的是第一项。这意味着" B" peek需要一个项目,并询问limit输出流,这意味着limit需要询问" A" peek,来源。我们会给出一个项目,并一直到forEach,然后您就可以获得第一行:

A1B1C1

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

因此,它并没有要求" 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));

再次,forEach要求第一项。这是传播回来的。但是当它到达skip时,它知道它必须从它的上游请求6个项目才能通过下游。因此它在" A"上游发出请求。 peek,使用它而不将其传递到下游,发出另一个请求,依此类推。所以" A" peek得到6个项目请求并产生6个打印件,但这些项目不会传递下来。

A1
A2
A3
A4
A5
A6

skip提出的第7个请求中,该项目被传递给" B"窥视并从它到forEach,所以完整的打印完成:

A7B7C7

然后就像以前一样。 skip现在,无论何时收到请求,都要求上游的项目并将其传递到下游,因为它知道"它已经完成了跳绳工作。所以其余的印刷品都要经过整个管道,直到光源耗尽为止。

答案 1 :(得分:10)

流水管道的流畅表示法导致了这种混乱。以这种方式思考:

limit(3)

所有流水线操作都是懒惰评估的,forEach()除外,terminal operation,触发"执行管道"

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

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个值传递给s4,并在第三个值之后中止管道。 s3不会产生更多值。 这并不意味着管道中没有更多的值。 s2仍会产生(并打印)更多值,但没有人请求这些值,因此执行停止。
  • s4再次查看传递它的所有值并打印它们。
  • forEach会消耗并打印任何s4次传递。

以这种方式思考。整个流是完全懒惰的。只有终端操作主动从管道中提取新值。从s4 <- s3 <- s2 <- s1中提取3个值后,s3将不再生成新值,并且不会再从s2 <- s1中提取任何值。虽然s1 -> s2仍然可以生成4-9,但这些值永远不会从管道中提取,因此永远不会被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个值&#34;跳过它们&#34; ,这意味着前6个值未传递给s4,只有后续的值才是。
  • s4再次查看传递它的所有值并打印它们。
  • forEach会消耗并打印任何s4次传递。

这里重要的是s2不知道剩余的管道正在跳过任何值。 s2独立于之后发生的事情,查看所有值。

另一个例子:

考虑这个管道,which is listed in this blog post

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生成无限量的交替值:01010,{{1} },...
  • 1会消耗之前遇到的所有值,仅传递&#34; new&#34; 值,即{{{}总共有2个值1}}。
  • i2传递10个值,然后停止。

此算法永远不会停止,因为i2等待i3i3i2之后再生成8个值,但这些值永远不会出现,而{{1}永远不会停止向0提供价值。

在管道中的某个点上,产生了超过10个值并不重要。重要的是1从未见过这10个值。

回答你的问题:

  

只是那个&#34;跳过之前的每个动作都被执行而不是每个人都在限制之前是#34;

不。执行i1i2之前的所有操作。在两次执行中,您都会获得i3 - skip()。但limit()可能会使管道短路,一旦发生了感兴趣的事件(达到限制),就会中止价值消耗。

答案 2 :(得分:8)

单独查看蒸汽操作完全是亵渎神灵,因为这不是评估流的方式。

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

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个流元素通过操作链,尽管处理了6个流元素。

答案 3 :(得分:4)

所有流都基于分裂器,它基本上有两个操作:前进(前进一个元素,类似于迭代器)和分割(将自己分成任意位置,适合并行处理)。您可以随时停止获取输入元素(由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

答案 4 :(得分:0)

也许这个小图有助于人们对流的处理方式有了一些自然的“感觉”。

第一行=>8=>=7= ... ===描述了流。元素1..8从左向右流动。有三个“窗口”:

  1. 在第一个窗口(peek A)中,您会看到所有内容
  2. 在第二个窗口(skip 6limit 3)中进行了一种过滤。第一个或最后一个元素都被“消除”了-表示未传递给进一步处理。
  3. 在第三个窗口中,您只能看到传递的那些项目

┌────────────────────────────────────────────────────────────────────────────┐ │ │ │▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸ │ │ 8 7 6 5 4 3 2 1 │ │▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸ ▲ ▸▸▸▸▸▸▸▸▸▸▸ ▲ ▸▸▸▸▸▸▸▸▸▸ ▲ ▸▸▸▸▸▸▸▸▸ │ │ │ │ │ │ │ │ skip 6 │ │ │ peek A limit 3 peek B │ └────────────────────────────────────────────────────────────────────────────┘

从技术上讲,这种解释中的所有内容(也许甚至不是全部)可能都不是完全正确的。但是,当我这样看时,我很清楚哪些物品到达了哪些串联指令中。