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

68
谈到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

为什么在这种情况下会执行动作A1A6?这一定与limit短路有状态中间操作有关,而skip则不是,但我不理解这个属性的实际影响。它只是意味着“在skip之前的每个操作都会被执行,而不是在limit之前的所有操作都会被执行”吗?


1
这更多关于流的惰性操作 - 直到需要时才会生成项目。 - RealSkeptic
@RealSkeptic 嗯...如果你想再争论一下,我很乐意学习 =) - Luigi Cortese
1
好的,skip() 是一个有状态的中间操作。但是,根据 Javadoc 的说法,短路操作是可能在有限时间内处理无限流的操作。这有点说得通,因为 skip() 可能需要创建一个不同的无限流,而 limit() 只需要取前几个。我不知道为什么这种差异(如果真的是这样)会影响您的输出。 - bcsb1001
5个回答

105
这里有两个流水线,每个流水线都由源、几个中间操作和一个终端操作组成。中间操作是惰性的,这意味着除非下游操作需要一个元素,否则不会发生任何事情。一旦需要,中间操作就会执行所需的所有操作来生成所需的元素,然后再次等待直到请求另一个元素,如此往复。终端操作通常是“渴望”的,即它们要求流中为完成它们所需的所有项目。因此,您应该真正将流水线视为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方法知道已经提供了所有允许提供的元素,因此不再请求下一个元素并立即指示其元素已经耗尽,然后终止执行。第二个流水线会发生什么?
    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发起的请求中,该项被传递到“B” peek,然后从中传递到forEach,因此完整的打印完成:

A7B7C7

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


我喜欢来回请求的图片,我认为你在这里抓住了重点。我会对此进行一些测试... - Luigi Cortese
9
一个重要的点是流的阶段没有随机访问,因此 skip 实际上不能跳过,而必须迭代请求的元素数量并将它们丢弃。理论上,当源是 SIZED 类型且可以预测需要跳过一半以上时,它可以尝试分割源。但在当前实现中,没有这样的优化。 - Holger
非常好的解释。 - Udayaditya Barua
当请求到达“限制”时,它知道它已经给出了所有允许给出的项目。显然,它甚至更早就知道了,因为它不会为第七个元素调用“peek”。理论上,“skip”应该能够类似地在源处跳过结果,只要没有介入的“filter”或“flatMap”来改变下游光标。 - shmosel

13

流式管道的流畅符号是导致这种混淆的原因。可以这样考虑:

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 将前三个值传递给 s4 并在第三个值后中止管道。 这并不意味着管道中没有更多的值。 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个值,"跳过它们",这意味着前6个值不传递给s4,只有随后的值才会传递给它。
  • s4 再次查看通过它的所有值并打印它们。
  • forEach 消耗并打印s4传递给它的任何元素。

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

另一个例子:

考虑这个流水线,该流水线在此博客文章中列出

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 生成一个无限数量的交替值:0、1、0、1、0、1…… - i2 消耗之前遇到的所有值,仅传递“新”的值,即总共有 2 个值从 i2 输出。 - i3 传递 10 个值,然后停止。
这个算法永远不会停止,因为 i3 等待 i2 在 0 和 1 之后产生更多的 8 个值,但这些值从未出现,而 i1 永远不会停止向 i2 提供值。
在管道中的某个时刻产生了超过 10 个值并不重要。重要的是 i3 从未看到那 10 个值。
回答你的问题:
“只是意思是在跳过之前执行每个操作,而在达到限制之前不执行每个操作吗?”
不是。在 skip() 或 limit() 之前的所有操作都会执行。在你的两个执行中,你得到 A1 - A3。但是,limit() 可能会中断管道,一旦发生感兴趣的事件(达到限制)就终止值的消耗。

1
@LuigiCortese:我已经用粗体澄清了一些部分。limit()将“短路流”,这意味着终端操作可以不再消耗任何值,因此执行停止。就像findFirst()一样。 - Lukas Eder
2
@LuigiCortese,这些值不再通过s2,因为管道被limit(3)“短路”了,一旦传递了3个值。终端操作forEach()停止从s4消耗更多的值,它从s3消耗它们,后者从s2消耗它们,后者从s1消耗它们。因此,值4-9永远不会被消耗。 - Lukas Eder
1
这就是我的意思。否则,如果管道之前仍然消耗整个可能无限的流,则“limit()”将毫无用处。 - Lukas Eder
2
@AmmSokun 嗯,我认为这是一个定义问题:在这种情况下,“独立”是什么意思? - Luigi Cortese
1
@AmmSokun:”total“量词也经常是“大错特错”的。但我理解你的意思,我会相应地重新审查我的回答。 - Lukas Eder
显示剩余7条评论

9

将steam操作单独看待是完全错误的,因为这不是评估steam的方式。

谈论limit(3)时,这是一种短路操作,这是有意义的,因为无论在limit之前和之后的任何操作,在stream中加入一个限制后,在达到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个流元素。


@RealSkeptic的回答消除了所有疑虑。现在我也理解你的例子了:可以处理无限数量的元素,直到最多有3个到达终端操作。我同意你的看法,单独查看流操作并没有什么帮助。 - Luigi Cortese

5

所有流都基于Spliterator,它有两个基本操作:advance(向前移动一个元素,类似于迭代器)和split(在任意位置分割自身,适合并行处理)。您可以随时停止接收输入元素(通过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

0
也许这个小图可以帮助你对流程如何处理有一些自然的“感觉”。
第一行=>8=>=7=... === 表示流。元素1..8从左到右流动。有三个“窗口”:
  1. 在第一个窗口(Peek A)中,您可以看到所有内容
  2. 在第二个窗口(Skip 6Limit 3)中进行了一种过滤。要么是第一个元素,要么是最后一个元素被“排除”-意味着不会继续传递进行处理。
  3. 在第三个窗口中,您只能看到已经传递的那些项目

в”Ңв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”җ в”Ӯ в”Ӯ в”Ӯв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ё в–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ё в–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ё в–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ё в”Ӯ в”Ӯ 8 7 6 5 4 3 2 1 в”Ӯ в”Ӯв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ё в–І в–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ё в–І в–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ё в–І в–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ёв–ё в”Ӯ в”Ӯ в”Ӯ в”Ӯ в”Ӯ в”Ӯ в”Ӯ в”Ӯ и·іиҝҮ6 в”Ӯ в”Ӯ в”Ӯ зӘҘи§ҶA йҷҗеҲ¶3 зӘҘи§ҶB в”Ӯ в””в”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”Җв”ҳ

可能并非所有内容(甚至可能没有任何内容)在技术上都是完全正确的解释。但当我看到它时,很清楚哪些项目达到了哪些连接指令。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接