[英]Java 8 Stream: difference between limit() and 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()
方法对最后三个元素执行类似计算,显示不同的行为: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
在这种情况下,为什么执行A1到A6的操作? 它必须与limit是一个短路状态中间操作这一事实有关,而skip不是,但我不明白这个属性的实际含义。 是不是“在跳过之前的所有动作都被执行而不是每个人都在限制之前”?
你在这里有两个流管道。
这些流管道每个都包含一个源,几个中间操作和一个终端操作。
但是中间操作是懒惰的。 这意味着除非下游操作需要项目,否则不会发生任何事情。 如果是这样,那么中间操作会完成生成所需项目所需的全部操作,然后再次等待直到请求另一个项目,依此类推。
终端操作通常是“急切的”。 也就是说,他们要求流中的所有项目完成所需的项目。
因此,您应该将管道视为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”偷看获得6个项目请求并产生6个打印,但这些项目不会传递下来。
A1
A2
A3
A4
A5
A6
在skip
的第7个请求中,项目向下传递到“B”,并从它传递到forEach
,因此完整打印完成:
A7B7C7
那就像以前一样。 skip
现在,无论何时获得请求,都要求上游项目并将其传递到下游,因为它“知道”它已经完成了跳过工作。 因此,其余的打印件将通过整个管道,直到源耗尽。
流水管道的流畅表示法正是造成这种混乱的原因。 以这种方式思考:
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个值传递给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个值, “跳过它们” ,这意味着前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
消耗之前遇到的所有值,仅传递“新”值,即i2
总共有2个值。 i3
传递10个值,然后停止。 这个算法永远不会停止,因为i3
在0
和1
之后等待i2
再产生8个值,但是这些值永远不会出现,而i1
永远不会停止向i2
值。
在管道中的某个点上,产生了超过10个值并不重要。 重要的是i3
从未见过这10个值。
是不是“在跳过之前的所有动作都被执行而不是每个人都在限制之前”?
不。 执行skip()
或limit()
之前的所有操作。 在两次执行中,你都获得了A1
- A3
。 但是limit()
可能会使管道短路,一旦发生了感兴趣的事件(达到限制),就会中止价值消耗。
单独查看蒸汽操作完全是亵渎神灵,因为这不是评估流的方式。
谈到限制(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个流元素。
所有流都基于分裂器,它基本上有两个操作:前进(向前移动一个元素,类似于迭代器)和分割(将自己划分到任意位置,这适合于并行处理)。 你可以随时停止输入元素(由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
也许这个小图有助于对流的处理方式产生一些自然的“感觉”。
第一行=>8=>=7=
... ===
描绘了流。 元素1..8从左向右流动。 有三个“窗口”:
peek A
)中,您可以看到一切 skip 6
或limit 3
)中进行一种过滤。 第一个或最后一个元素被“消除” - 意味着没有传递进行进一步处理。 ┌────────────────────────────────────────────────────────────────────────────┐ │ │ │▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸ │ │ 8 7 6 5 4 3 2 1 │ │▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸ ▲ ▸▸▸▸▸▸▸▸▸▸▸ ▲ ▸▸▸▸▸▸▸▸▸▸ ▲ ▸▸▸▸▸▸▸▸▸ │ │ │ │ │ │ │ │ skip 6 │ │ │ peek A limit 3 peek B │ └────────────────────────────────────────────────────────────────────────────┘
可能不是这个解释中的所有东西(甚至可能不是任何东西)在技术上都是完全正确的。 但是当我看到这样的情况时,我很清楚哪些项目可以达到哪种连接指令。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.