为什么 Java Streams 是一次性的?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/28459498/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Why are Java Streams once-off?
提问by Vitaliy
Unlike C#'s IEnumerable
, where an execution pipeline can be executed as many times as we want, in Java a stream can be 'iterated' only once.
与 C# 不同IEnumerable
,在C# 中,可以根据需要多次执行执行管道,而在 Java 中,流只能“迭代”一次。
Any call to a terminal operation closes the stream, rendering it unusable. This 'feature' takes away a lot of power.
对终端操作的任何调用都会关闭流,使其无法使用。这个“功能”带走了很多力量。
I imagine the reason for this is nottechnical. What were the design considerations behind this strange restriction?
我想这不是技术原因。这个奇怪的限制背后的设计考虑是什么?
Edit: in order to demonstrate what I am talking about, consider the following implementation of Quick-Sort in C#:
编辑:为了演示我在说什么,请考虑以下 C# 中快速排序的实现:
IEnumerable<int> QuickSort(IEnumerable<int> ints)
{
if (!ints.Any()) {
return Enumerable.Empty<int>();
}
int pivot = ints.First();
IEnumerable<int> lt = ints.Where(i => i < pivot);
IEnumerable<int> gt = ints.Where(i => i > pivot);
return QuickSort(lt).Concat(new int[] { pivot }).Concat(QuickSort(gt));
}
Now to be sure, I am not advocating that this is a good implementation of quick sort! It is however great example of the expressive power of lambda expression combined with stream operation.
现在可以肯定的是,我并不是在提倡这是快速排序的一个很好的实现!然而,它是 lambda 表达式与流操作相结合的表现力的一个很好的例子。
And it can't be done in Java! I can't even ask a stream whether it is empty without rendering it unusable.
它不能在Java中完成!我什至不能在不使其无法使用的情况下询问流是否为空。
采纳答案by Stuart Marks
I have some recollections from the early design of the Streams API that might shed some light on the design rationale.
我对 Streams API 的早期设计有一些回忆,这些回忆可能会阐明设计原理。
Back in 2012, we were adding lambdas to the language, and we wanted a collections-oriented or "bulk data" set of operations, programmed using lambdas, that would facilitate parallelism. The idea of lazily chaining operations together was well established by this point. We also didn't want the intermediate operations to store results.
早在 2012 年,我们就在语言中添加了 lambda,我们想要一个面向集合或“批量数据”的操作集,使用 lambda 进行编程,以促进并行性。在这一点上,懒惰地将操作链接在一起的想法已经很好地建立了。我们也不希望中间操作存储结果。
The main issues we needed to decide were what the objects in the chain looked like in the API and how they hooked up to data sources. The sources were often collections, but we also wanted to support data coming from a file or the network, or data generated on-the-fly, e.g., from a random number generator.
我们需要决定的主要问题是链中的对象在 API 中的样子以及它们如何连接到数据源。来源通常是集合,但我们也希望支持来自文件或网络的数据,或者即时生成的数据,例如来自随机数生成器的数据。
There were many influences of existing work on the design. Among the more influential were Google's Guavalibrary and the Scala collections library. (If anybody is surprised about the influence from Guava, note that Kevin Bourrillion, Guava lead developer, was on the JSR-335 Lambdaexpert group.) On Scala collections, we found this talk by Martin Odersky to be of particular interest: Future-Proofing Scala Collections: from Mutable to Persistent to Parallel. (Stanford EE380, 2011 June 1.)
现有工作对设计有很多影响。其中比较有影响的是谷歌的Guava库和 Scala 集合库。(如果有人对 Guava 的影响感到惊讶,请注意Guava 首席开发人员Kevin Bourrillion是JSR-335 Lambda专家组的成员。)关于 Scala 集合,我们发现 Martin Odersky 的这个演讲特别有趣:Future-校对 Scala 集合:从可变到持久再到并行。(斯坦福 EE380,2011 年 6 月 1 日。)
Our prototype design at the time was based around Iterable
. The familiar operations filter
, map
, and so forth were extension (default) methods on Iterable
. Calling one added an operation to the chain and returned another Iterable
. A terminal operation like count
would call iterator()
up the chain to the source, and the operations were implemented within each stage's Iterator.
我们当时的原型设计是基于Iterable
. 熟悉的filter
、map
等操作是 上的扩展(默认)方法Iterable
。调用一个向链中添加了一个操作并返回了另一个Iterable
。类似的终端操作count
将调用iterator()
链到源,并且这些操作在每个阶段的迭代器中实现。
Since these are Iterables, you can call the iterator()
method more than once. What should happen then?
由于这些是可迭代对象,因此您可以iterator()
多次调用该方法。那应该怎么办?
If the source is a collection, this mostly works fine. Collections are Iterable, and each call to iterator()
produces a distinct Iterator instance that is independent of any other active instances, and each traverses the collection independently. Great.
如果源是一个集合,这通常可以正常工作。集合是可iterator()
迭代的,每次调用都会产生一个独立于任何其他活动实例的独特 Iterator 实例,并且每个都独立地遍历集合。伟大的。
Now what if the source is one-shot, like reading lines from a file? Maybe the first Iterator should get all the values but the second and subsequent ones should be empty. Maybe the values should be interleaved among the Iterators. Or maybe each Iterator should get all the same values. Then, what if you have two iterators and one gets farther ahead of the other? Somebody will have to buffer up the values in the second Iterator until they're read. Worse, what if you get one Iterator and read all the values, and only thenget a second Iterator. Where do the values come from now? Is there a requirement for them all to be buffered up just in casesomebody wants a second Iterator?
现在如果源是一次性的,比如从文件中读取行怎么办?也许第一个迭代器应该得到所有的值,但第二个和后续的应该是空的。也许这些值应该在迭代器之间交错。或者也许每个迭代器都应该获得所有相同的值。那么,如果你有两个迭代器并且一个比另一个更早呢?有人将不得不缓冲第二个迭代器中的值,直到它们被读取。更糟的是,如果你明白一个迭代器和读取所有的值,只有再获得第二个迭代器。价值从何而来?是否需要将它们全部缓冲以防万一有人想要第二个迭代器?
Clearly, allowing multiple Iterators over a one-shot source raises a lot of questions. We didn't have good answers for them. We wanted consistent, predictable behavior for what happens if you call iterator()
twice. This pushed us toward disallowing multiple traversals, making the pipelines one-shot.
显然,在一次性源上允许多个迭代器会引发很多问题。我们没有给他们很好的答案。如果您调用iterator()
两次,我们想要一致的、可预测的行为。这促使我们禁止多次遍历,使管道一次性。
We also observed others bumping into these issues. In the JDK, most Iterables are collections or collection-like objects, which allow multiple traversal. It isn't specified anywhere, but there seemed to be an unwritten expectation that Iterables allow multiple traversal. A notable exception is the NIO DirectoryStreaminterface. Its specification includes this interesting warning:
我们还观察到其他人遇到了这些问题。在 JDK 中,大多数 Iterable 都是集合或类集合对象,允许多次遍历。它没有在任何地方指定,但似乎有一个不成文的期望,即 Iterables 允许多次遍历。一个值得注意的例外是 NIO DirectoryStream接口。它的规范包括这个有趣的警告:
While DirectoryStream extends Iterable, it is not a general-purpose Iterable as it supports only a single Iterator; invoking the iterator method to obtain a second or subsequent iterator throws IllegalStateException.
虽然 DirectoryStream 扩展了 Iterable,但它不是通用的 Iterable,因为它只支持单个 Iterator;调用迭代器方法以获取第二个或后续迭代器会抛出 IllegalStateException。
[bold in original]
【原文加粗】
This seemed unusual and unpleasant enough that we didn't want to create a whole bunch of new Iterables that might be once-only. This pushed us away from using Iterable.
这看起来很不寻常和令人不快,以至于我们不想创建一大堆可能只有一次的新 Iterable。这促使我们远离使用 Iterable。
About this time, an article by Bruce Eckelappeared that described a spot of trouble he'd had with Scala. He'd written this code:
大约在这个时候,Bruce Eckel发表了一篇文章,描述了他在使用 Scala 时遇到的一个问题。他写了这样的代码:
// Scala
val lines = fromString(data).getLines
val registrants = lines.map(Registrant)
registrants.foreach(println)
registrants.foreach(println)
It's pretty straightforward. It parses lines of text into Registrant
objects and prints them out twice. Except that it actually only prints them out once. It turns out that he thought that registrants
was a collection, when in fact it's an iterator. The second call to foreach
encounters an empty iterator, from which all values have been exhausted, so it prints nothing.
这很简单。它将文本行解析为Registrant
对象并将它们打印两次。除了它实际上只打印一次。事实证明,他认为这registrants
是一个集合,而实际上它是一个迭代器。第二次调用foreach
遇到一个空迭代器,其中所有值都已耗尽,因此它什么也不打印。
This kind of experience convinced us that it was very important to have clearly predictable results if multiple traversal is attempted. It also highlighted the importance of distinguishing between lazy pipeline-like structures from actual collections that store data. This in turn drove the separation of the lazy pipeline operations into the new Stream interface and keeping only eager, mutative operations directly on Collections. Brian Goetz has explainedthe rationale for that.
这种经验使我们确信,如果尝试多次遍历,获得清晰可预测的结果是非常重要的。它还强调了将类似惰性管道的结构与存储数据的实际集合区分开来的重要性。这反过来又推动了将惰性管道操作分离到新的 Stream 接口中,并仅在集合上直接保留急切的、可变的操作。布赖恩·戈茨 (Brian Goetz) 解释了这样做的理由。
What about allowing multiple traversal for collection-based pipelines but disallowing it for non-collection-based pipelines? It's inconsistent, but it's sensible. If you're reading values from the network, of courseyou can't traverse them again. If you want to traverse them multiple times, you have to pull them into a collection explicitly.
允许对基于集合的管道进行多次遍历,但不允许对非基于集合的管道进行多次遍历呢?这是矛盾的,但它是明智的。如果您正在从网络读取值,当然您不能再次遍历它们。如果您想多次遍历它们,则必须明确地将它们拉入一个集合中。
But let's explore allowing multiple traversal from collections-based pipelines. Let's say you did this:
但是让我们探索允许从基于集合的管道进行多次遍历。假设你这样做了:
Iterable<?> it = source.filter(...).map(...).filter(...).map(...);
it.into(dest1);
it.into(dest2);
(The into
operation is now spelled collect(toList())
.)
(该into
操作现在拼写为collect(toList())
。)
If source is a collection, then the first into()
call will create a chain of Iterators back to the source, execute the pipeline operations, and send the results into the destination. The second call to into()
will create another chain of Iterators, and execute the pipeline operations again. This isn't obviously wrong but it does have the effect of performing all the filter and map operations a second time for each element. I think many programmers would have been surprised by this behavior.
如果源是一个集合,那么第一次into()
调用将创建一个返回源的迭代器链,执行管道操作,并将结果发送到目标。第二次调用into()
将创建另一个迭代器链,并再次执行管道操作。这显然没有错,但它确实具有为每个元素第二次执行所有过滤器和映射操作的效果。我想很多程序员都会对这种行为感到惊讶。
As I mentioned above, we had been talking to the Guava developers. One of the cool things they have is an Idea Graveyardwhere they describe features that they decided notto implement along with the reasons. The idea of lazy collections sounds pretty cool, but here's what they have to say about it. Consider a List.filter()
operation that returns a List
:
正如我上面提到的,我们一直在与 Guava 开发人员交谈。他们拥有的一项很酷的东西是Idea Graveyard,在那里他们描述了他们决定不实施的功能以及原因。惰性集合的想法听起来很酷,但这是他们不得不说的。考虑一个List.filter()
返回 a的操作List
:
The biggest concern here is that too many operations become expensive, linear-time propositions. If you want to filter a list and get a list back, and not just a Collection or an Iterable, you can use
ImmutableList.copyOf(Iterables.filter(list, predicate))
, which "states up front" what it's doing and how expensive it is.
这里最大的问题是太多的操作变成了昂贵的线性时间命题。如果你想过滤一个列表并返回一个列表,而不仅仅是一个集合或一个可迭代的,你可以使用
ImmutableList.copyOf(Iterables.filter(list, predicate))
,它“预先说明”它在做什么以及它有多昂贵。
To take a specific example, what's the cost of get(0)
or size()
on a List? For commonly used classes like ArrayList
, they're O(1). But if you call one of these on a lazily-filtered list, it has to run the filter over the backing list, and all of a sudden these operations are O(n). Worse, it has to traverse the backing list on everyoperation.
举一个具体的例子,什么是成本get(0)
或size()
上的列表?对于常用的类,如ArrayList
,它们是 O(1)。但是如果你在一个延迟过滤的列表上调用其中一个,它必须在支持列表上运行过滤器,突然这些操作是 O(n)。更糟糕的是,它必须遍历每个操作的支持列表。
This seemed to us to be too muchlaziness. It's one thing to set up some operations and defer actual execution until you so "Go". It's another to set things up in such a way that hides a potentially large amount of recomputation.
在我们看来,这太懒惰了。设置一些操作并将实际执行推迟到您“开始”之前是一回事。以隐藏大量重新计算的方式进行设置是另一种方式。
In proposing to disallow non-linear or "no-reuse" streams, Paul Sandozdescribed the potential consequencesof allowing them as giving rise to "unexpected or confusing results." He also mentioned that parallel execution would make things even trickier. Finally, I'd add that a pipeline operation with side effects would lead to difficult and obscure bugs if the operation were unexpectedly executed multiple times, or at least a different number of times than the programmer expected. (But Java programmers don't write lambda expressions with side effects, do they? DO THEY??)
在提议禁止非线性或“不可重用”流时,Paul Sandoz将允许它们的潜在后果描述为导致“意外或混乱的结果”。他还提到并行执行会使事情变得更加棘手。最后,我要补充一点,如果操作意外执行多次,或者至少与程序员预期的次数不同,那么带有副作用的管道操作将导致困难和模糊的错误。(但 Java 程序员不会编写带有副作用的 lambda 表达式,是吗?他们会吗??)
So that's the basic rationale for the Java 8 Streams API design that allows one-shot traversal and that requires a strictly linear (no branching) pipeline. It provides consistent behavior across multiple different stream sources, it clearly separates lazy from eager operations, and it provides a straightforward execution model.
这就是 Java 8 Streams API 设计的基本原理,它允许一次性遍历并且需要严格的线性(无分支)管道。它在多个不同的流源之间提供一致的行为,它清楚地将惰性操作与急切操作区分开来,并且它提供了一个简单的执行模型。
With regard to IEnumerable
, I am far from an expert on C# and .NET, so I would appreciate being corrected (gently) if I draw any incorrect conclusions. It does appear, however, that IEnumerable
permits multiple traversal to behave differently with different sources; and it permits a branching structure of nested IEnumerable
operations, which may result in some significant recomputation. While I appreciate that different systems make different tradeoffs, these are two characteristics that we sought to avoid in the design of the Java 8 Streams API.
关于IEnumerable
,我远非 C# 和 .NET方面的专家,因此如果我得出任何不正确的结论,我希望得到纠正(温和地)。然而,它似乎IEnumerable
允许多次遍历对不同的源有不同的表现;它允许嵌套IEnumerable
操作的分支结构,这可能会导致一些重要的重新计算。虽然我理解不同的系统会做出不同的权衡,但这是我们在 Java 8 Streams API 设计中试图避免的两个特征。
The quicksort example given by the OP is interesting, puzzling, and I'm sorry to say, somewhat horrifying. Calling QuickSort
takes an IEnumerable
and returns an IEnumerable
, so no sorting is actually done until the final IEnumerable
is traversed. What the call seems to do, though, is build up a tree structure of IEnumerables
that reflects the partitioning that quicksort would do, without actually doing it. (This is lazy computation, after all.) If the source has N elements, the tree will be N elements wide at its widest, and it will be lg(N) levels deep.
OP 给出的快速排序示例很有趣,令人费解,而且很抱歉,有点可怕。调用QuickSort
接受 anIEnumerable
并返回 an IEnumerable
,因此在IEnumerable
遍历final 之前实际上不会进行排序。然而,这个调用似乎做的是建立一个树结构,IEnumerables
它反映了快速排序会做的分区,而不是实际做。(毕竟这是惰性计算。)如果源有 N 个元素,则树的最宽将是 N 个元素宽,并且深度为 lg(N) 级。
It seems to me -- and once again, I'm not a C# or .NET expert -- that this will cause certain innocuous-looking calls, such as pivot selection via ints.First()
, to be more expensive than they look. At the first level, of course, it's O(1). But consider a partition deep in the tree, at the right-hand edge. To compute the first element of this partition, the entire source has to be traversed, an O(N) operation. But since the partitions above are lazy, they must be recomputed, requiring O(lg N) comparisons. So selecting the pivot would be an O(N lg N) operation, which is as expensive as an entire sort.
在我看来——再一次,我不是 C# 或 .NET 专家——这将导致某些看起来无害的调用,例如通过 的枢轴选择ints.First()
,比它们看起来更昂贵。在第一层,当然是 O(1)。但是考虑在树深处的右侧边缘的分区。要计算此分区的第一个元素,必须遍历整个源,这是一个 O(N) 操作。但是由于上面的分区是惰性的,它们必须重新计算,需要 O(lg N) 次比较。因此,选择主元将是一个 O(N lg N) 操作,这与整个排序一样昂贵。
But we don't actually sort until we traverse the returned IEnumerable
. In the standard quicksort algorithm, each level of partitioning doubles the number of partitions. Each partition is only half the size, so each level remains at O(N) complexity. The tree of partitions is O(lg N) high, so the total work is O(N lg N).
但是在遍历返回的IEnumerable
. 在标准的快速排序算法中,每一级分区都会使分区数加倍。每个分区只有一半的大小,因此每个级别保持 O(N) 复杂度。分区树的高度为 O(lg N),因此总工作量是 O(N lg N)。
With the tree of lazy IEnumerables, at the bottom of the tree there are N partitions. Computing each partition requires a traversal of N elements, each of which requires lg(N) comparisons up the tree. To compute all the partitions at the bottom of the tree, then, requires O(N^2 lg N) comparisons.
对于惰性 IEnumerables 树,在树的底部有 N 个分区。计算每个分区需要遍历 N 个元素,每个元素都需要在树上进行 lg(N) 次比较。为了计算树底部的所有分区,需要 O(N^2 lg N) 次比较。
(Is this right? I can hardly believe this. Somebody please check this for me.)
(这是对的吗?我简直不敢相信。请有人帮我检查一下。)
In any case, it is indeed cool that IEnumerable
can be used this way to build up complicated structures of computation. But if it does increase the computational complexity as much as I think it does, it would seem that programming this way is something that should be avoided unless one is extremely careful.
无论如何,IEnumerable
可以用这种方式构建复杂的计算结构确实很酷。但是,如果它确实像我认为的那样增加了计算复杂性,那么除非非常小心,否则似乎应该避免以这种方式编程。
回答by rolfl
Background
背景
While the question appears simple, the actual answer requires some background to make sense. If you want to skip to the conclusion, scroll down...
虽然这个问题看起来很简单,但实际答案需要一些背景知识才能有意义。如果你想跳到结论,向下滚动...
Pick your comparison point - Basic functionality
选择您的比较点 - 基本功能
Using basic concepts, C#'s IEnumerable
concept is more closely related to Java's Iterable
, which is able to create as many Iteratorsas you want. IEnumerables
create IEnumerators
. Java's Iterable
create Iterators
使用基本概念,C# 的IEnumerable
概念与Java 的Iterable
更密切相关,它能够创建任意数量的迭代器。IEnumerables
创建IEnumerators
. Java的Iterable
创建Iterators
The history of each concept is similar, in that both IEnumerable
and Iterable
have a basic motivation to allow 'for-each' style looping over the members of data collections. That's an oversimplification as they both allow more than just that, and they also arrived at that stage via different progressions, but it is a significant common feature regardless.
每个概念的历史是相似的,在这两个IEnumerable
和Iterable
有一个基本的动机,让“换每个”风格遍历数据收集的成员。这过于简单化了,因为它们都允许更多,而且它们也通过不同的进程到达那个阶段,但无论如何它都是一个重要的共同特征。
Let's compare that feature: in both languages, if a class implements the IEnumerable
/Iterable
, then that class must implement at least a single method (for C#, it's GetEnumerator
and for Java it's iterator()
). In each case, the instance returned from that (IEnumerator
/Iterator
) allows you to access the current and subsequent members of the data. This feature is used in the for-each language syntax.
让我们比较一下这个特性:在两种语言中,如果一个类实现了IEnumerable
/ Iterable
,那么该类必须至少实现一个方法(对于 C#,它是GetEnumerator
,对于 Java,它是iterator()
)。在每种情况下,从该 ( IEnumerator
/ Iterator
)返回的实例都允许您访问数据的当前成员和后续成员。此功能用于 for-each 语言语法中。
Pick your comparison point - Enhanced functionality
选择您的比较点 - 增强功能
IEnumerable
in C# has been extended to allow a number of other language features (mostly related to Linq). Features added include selections, projections, aggregations, etc. These extensions have a strong motivation from use in set-theory, similar to SQL and Relational Database concepts.
IEnumerable
在 C# 中已被扩展以允许许多其他语言功能(主要与 Linq 相关)。添加的功能包括选择、投影、聚合等。这些扩展在集合论中具有很强的使用动机,类似于 SQL 和关系数据库概念。
Java 8 has also had functionality added to enable a degree of functional programming using Streams and Lambdas. Note that Java 8 streams are not primarily motivated by set theory, but by functional programming. Regardless, there are a lot of parallels.
Java 8 还添加了一些功能,以支持使用 Streams 和 Lambdas 进行一定程度的函数式编程。请注意,Java 8 流的主要动机不是集合论,而是函数式编程。无论如何,有很多相似之处。
So, this is the second point. The enhancements made to C# were implemented as an enhancement to the IEnumerable
concept. In Java, though, the enhancements made were implemented by creating new base concepts of Lambdas and Streams, and then also creating a relatively trivial way to convert from Iterators
and Iterables
to Streams, and visa-versa.
所以,这是第二点。对 C# 所做的增强是作为对IEnumerable
概念的增强而实现的。然而,在 Java 中,所做的增强是通过创建 Lambdas 和 Streams 的新基本概念来实现的,然后还创建了一种相对简单的方式来从Iterators
和Iterables
到 Streams,反之亦然。
So, comparing IEnumerable to Java's Stream concept is incomplete. You need to compare it to the combined Streams and Collections API's in Java.
因此,将 IEnumerable 与 Java 的 Stream 概念进行比较是不完整的。您需要将其与 Java 中组合的 Streams 和 Collections API 进行比较。
In Java, Streams are not the same as Iterables, or Iterators
在 Java 中,Streams 与 Iterables 或 Iterators 不同
Streams are not designed to solve problems the same way that iterators are:
流的设计目的不是像迭代器那样解决问题:
- Iterators are a way of describing the sequence of data.
- Streams are a way of describing a sequence of data transformations.
- 迭代器是一种描述数据序列的方式。
- 流是描述数据转换序列的一种方式。
With an Iterator
, you get a data value, process it, and then get another data value.
使用Iterator
,您可以获得一个数据值,对其进行处理,然后获得另一个数据值。
With Streams, you chain a sequence of functions together, then you feed an input value to the stream, and get the output value from the combined sequence. Note, in Java terms, each function is encapsulated in a single Stream
instance. The Streams API allows you to link a sequence of Stream
instances in a way that chains a sequence of transformation expressions.
使用 Streams,您将一系列函数链接在一起,然后将输入值提供给流,并从组合序列中获取输出值。请注意,在 Java 术语中,每个函数都封装在单个Stream
实例中。Streams API 允许您以链接Stream
一系列转换表达式的方式链接一系列实例。
In order to complete the Stream
concept, you need a source of data to feed the stream, and a terminal function that consumes the stream.
为了完成这个Stream
概念,您需要一个数据源来提供流,以及一个使用流的终端函数。
The way you feed values in to the stream may in fact be from an Iterable
, but the Stream
sequence itself is not an Iterable
, it is a compound function.
您将值输入到流中的方式实际上可能来自 an Iterable
,但Stream
序列本身不是 an Iterable
,它是一个复合函数。
A Stream
is also intended to be lazy, in the sense that it only does work when you request a value from it.
AStream
也是惰性的,因为它仅在您向其请求值时才起作用。
Note these significant assumptions and features of Streams:
请注意 Streams 的这些重要假设和特性:
- A
Stream
in Java is a transformation engine, it transforms a data item in one state, to being in another state. - streams have no concept of the data order or position, the simply transform whatever they are asked to.
- streams can be supplied with data from many sources, including other streams, Iterators, Iterables, Collections,
- you cannot "reset" a stream, that would be like "reprogramming the transformation". Resetting the data source is probably what you want.
- there is logically only 1 data item 'in flight' in the stream at any time (unless the stream is a parallel stream, at which point, there is 1 item per thread). This is independent of the data source which may have more than the current items 'ready' to be supplied to the stream, or the stream collector which may need to aggregate and reduce multiple values.
- Streams can be unbound (infinite), limited only by the data source, or collector (which can be infinite too).
- Streams are 'chainable', the output of filtering one stream, is another stream. Values input to and transformed by a stream can in turn be supplied to another stream which does a different transformation. The data, in its transformed state flows from one stream to the next. You do not need to intervene and pull the data from one stream and plug it in to the next.
Stream
Java 中的A是一个转换引擎,它将处于一种状态的数据项转换为另一种状态。- 流没有数据顺序或位置的概念,只需转换它们被要求的任何内容。
- 可以为流提供来自许多来源的数据,包括其他流、迭代器、可迭代对象、集合、
- 您不能“重置”流,就像“重新编程转换”一样。重置数据源可能是您想要的。
- 在任何时候,流中逻辑上只有 1 个数据项“正在运行”(除非流是并行流,此时每个线程有 1 个项)。这与数据源无关,数据源可能比“准备好”要提供给流的项目更多,或者与可能需要聚合和减少多个值的流收集器无关。
- 流可以是未绑定的(无限),仅受数据源或收集器(也可以是无限的)的限制。
- 流是“可链接的”,过滤一个流的输出是另一个流。输入到流并由流转换的值可以依次提供给进行不同转换的另一个流。处于转换状态的数据从一个流流到下一个流。您无需干预并从一个流中提取数据并将其插入下一个流。
C# Comparison
C# 比较
When you consider that a Java Stream is just a part of a supply, stream, and collect system, and that Streams and Iterators are often used together with Collections, then it is no wonder that it is hard to relate to the same concepts which are almost all embedded in to a single IEnumerable
concept in C#.
当您认为 Java Stream 只是供应、流和收集系统的一部分,并且 Streams 和 Iterators 经常与 Collections 一起使用时,难怪很难将相同的概念联系起来几乎所有内容都嵌入到IEnumerable
C# 中的单个概念中。
Parts of IEnumerable (and close related concepts) are apparent in all of the Java Iterator, Iterable, Lambda, and Stream concepts.
IEnumerable 的部分(和密切相关的概念)在所有 Java Iterator、Iterable、Lambda 和 Stream 概念中都很明显。
There are small things that the Java concepts can do that are harder in IEnumerable, and visa-versa.
Java 概念可以做的一些小事情在 IEnumerable 中更难,反之亦然。
Conclusion
结论
- There's no design problem here, just a problem in matching concepts between the languages.
- Streams solve problems in a different way
- Streams add functionality to Java (they add a different way of doing things, they do not take functionality away)
- 这里没有设计问题,只是语言之间的概念匹配问题。
- 流以不同的方式解决问题
- 流向 Java 添加了功能(它们添加了不同的做事方式,它们不会带走功能)
Adding Streams gives you more choices when solving problems, which is fair to classify as 'enhancing power', not 'reducing', 'taking away', or 'restricting' it.
添加Streams在解决问题时为您提供了更多选择,将其归类为“增强力量”,而不是“减少”、“带走”或“限制”它是公平的。
Why are Java Streams once-off?
为什么 Java Streams 是一次性的?
This question is misguided, because streams are function sequences, not data. Depending on the data source that feeds the stream, you can reset the data source, and feed the same, or different stream.
这个问题被误导了,因为流是函数序列,而不是数据。根据提供流的数据源,您可以重置数据源,并提供相同或不同的流。
Unlike C#'s IEnumerable, where an execution pipeline can be executed as many times as we want, in Java a stream can be 'iterated' only once.
与 C# 的 IEnumerable 不同,执行管道可以根据需要执行多次,而在 Java 中,流只能“迭代”一次。
Comparing an IEnumerable
to a Stream
is misguided. The context you are using to say IEnumerable
can be executed as many times as you want, is best compared to Java Iterables
, which can be iterated as many times as you want. A Java Stream
represents a subset of the IEnumerable
concept, and not the subset that supplies data, and thus cannot be 'rerun'.
将 anIEnumerable
与 a进行比较Stream
是错误的。您用来表示的上下文IEnumerable
可以根据需要执行多次,最好与 Java 相比,JavaIterables
可以根据需要迭代多次。JavaStream
代表IEnumerable
概念的一个子集,而不是提供数据的子集,因此不能“重新运行”。
Any call to a terminal operation closes the stream, rendering it unusable. This 'feature' takes away a lot of power.
对终端操作的任何调用都会关闭流,使其无法使用。这个“功能”带走了很多力量。
The first statement is true, in a sense. The 'takes away power' statement is not. You are still comparing Streams it IEnumerables. The terminal operation in the stream is like a 'break' clause in a for loop. You are always free to have another stream, if you want, and if you can re-supply the data you need. Again, if you consider the IEnumerable
to be more like an Iterable
, for this statement, Java does it just fine.
从某种意义上说,第一个陈述是正确的。“夺走权力”的说法不是。您仍在比较 Streams it IEnumerables。流中的终止操作就像 for 循环中的“break”子句。如果您愿意,并且可以重新提供所需的数据,您始终可以自由拥有另一个流。同样,如果您认为IEnumerable
更像是Iterable
,对于这个语句,Java 做得很好。
I imagine the reason for this is not technical. What were the design considerations behind this strange restriction?
我想这不是技术原因。这个奇怪的限制背后的设计考虑是什么?
The reason is technical, and for the simple reason that a Stream a subset of what think it is. The stream subset does not control the data supply, so you should reset the supply, not the stream. In that context, it is not so strange.
原因是技术性的,原因很简单,Stream 是它所认为的一个子集。流子集不控制数据供应,因此您应该重置供应,而不是流。在这种情况下,这并不奇怪。
QuickSort example
快速排序示例
Your quicksort example has the signature:
您的快速排序示例具有以下签名:
IEnumerable<int> QuickSort(IEnumerable<int> ints)
You are treating the input IEnumerable
as a data source:
您将输入IEnumerable
视为数据源:
IEnumerable<int> lt = ints.Where(i => i < pivot);
Additionally, return value is IEnumerable
too, which is a supply of data, and since this is a Sort operation, the order of that supply is significant. If you consider the Java Iterable
class to be the appropriate match for this, specifically the List
specialization of Iterable
, since List is a supply of data which has a guaranteed order or iteration, then the equivalent Java code to your code would be:
此外,返回值也是IEnumerable
如此,它是数据的供应,并且由于这是一个排序操作,该供应的顺序很重要。如果您认为 JavaIterable
类与此匹配,特别是List
的特化Iterable
,因为 List 是具有保证顺序或迭代的数据供应,那么与您的代码等效的 Java 代码将是:
Stream<Integer> quickSort(List<Integer> ints) {
// Using a stream to access the data, instead of the simpler ints.isEmpty()
if (!ints.stream().findAny().isPresent()) {
return Stream.of();
}
// treating the ints as a data collection, just like the C#
final Integer pivot = ints.get(0);
// Using streams to get the two partitions
List<Integer> lt = ints.stream().filter(i -> i < pivot).collect(Collectors.toList());
List<Integer> gt = ints.stream().filter(i -> i > pivot).collect(Collectors.toList());
return Stream.concat(Stream.concat(quickSort(lt), Stream.of(pivot)),quickSort(gt));
}
Note there is a bug (which I have reproduced), in that the sort does not handle duplicate values gracefully, it is a 'unique value' sort.
请注意,有一个错误(我已复制),因为排序不能正常处理重复值,它是一种“唯一值”排序。
Also note how the Java code uses data source (List
), and stream concepts at different point, and that in C# those two 'personalities' can be expressed in just IEnumerable
. Also, although I have use List
as the base type, I could have used the more general Collection
, and with a small iterator-to-Stream conversion, I could have used the even more general Iterable
还要注意 Java 代码如何List
在不同的点使用数据源 ( ) 和流概念,并且在 C# 中,这两种“个性”可以仅用IEnumerable
. 此外,虽然我使用List
了基本类型,但我可以使用更通用的Collection
,并且通过一个小的迭代器到流的转换,我可以使用更通用的Iterable
回答by Holger
Stream
s are built around Spliterator
s which are stateful, mutable objects. They don't have a “reset” action and in fact, requiring to support such rewind action would “take away much power”. How would Random.ints()
be supposed to handle such a request?
Stream
s 是围绕Spliterator
s构建的,s 是有状态的、可变的对象。他们没有“重置”动作,事实上,要求支持这种倒带动作会“带走很多力量”。怎么会Random.ints()
被认为来处理这样的要求?
On the other hand, for Stream
s which have a retraceable origin, it is easy to construct an equivalent Stream
to be used again. Just put the steps made to construct the Stream
into a reusable method. Keep in mind that repeating these steps is not an expensive operation as all these steps are lazy operations; the actual work starts with the terminal operation and depending on the actual terminal operation entirely different code might get executed.
另一方面,对于Stream
具有可追溯原点的 s,很容易构造一个等价物Stream
以供再次使用。只需将构建 的步骤Stream
放入可重用的方法中。请记住,重复这些步骤并不是一项昂贵的操作,因为所有这些步骤都是惰性操作;实际工作从终端操作开始,根据实际的终端操作,可能会执行完全不同的代码。
It would be up to you, the writer of such a method, to specify what calling the method twice implies: does it reproduce exactly the same sequence, as streams created for an unmodified array or collection do, or does it produce a stream with a similar semantics but different elements like a stream of random ints or a stream of console input lines, etc.
由您作为这种方法的作者来指定两次调用该方法意味着什么:它是否重现了完全相同的序列,就像为未修改的数组或集合创建的流所做的那样,或者它是否生成了一个带有类似的语义但不同的元素,如随机整数流或控制台输入行流等。
By the way, to avoid confusion, a terminal operation consumesthe Stream
which is distinct from closingthe Stream
as calling close()
on the stream does (which is required for streams having associated resources like, e.g. produced by Files.lines()
).
顺便说一下,为了避免混淆,终端操作消耗的Stream
是从不同的闭合的Stream
作为调用close()
流上不(这是需要的流具有相关联的喜欢的产生,例如资源Files.lines()
)。
It seems that a lot of confusion stems from misguiding comparison of IEnumerable
with Stream
. An IEnumerable
represents the ability to provide an actual IEnumerator
, so its like an Iterable
in Java. In contrast, a Stream
is a kind of iterator and comparable to an IEnumerator
so it's wrong to claim that this kind of data type can be used multiple times in .NET, the support for IEnumerator.Reset
is optional. The examples discussed here rather use the fact that an IEnumerable
can be used to fetch newIEnumerator
s and that works with Java's Collection
s as well; you can get a new Stream
. If the Java developers decided to add the Stream
operations to Iterable
directly, with intermediate operations returning another Iterable
, it was really comparable and it could work the same way.
似乎很多混淆源于对IEnumerable
with 的误导性比较Stream
。AnIEnumerable
代表提供实际 的能力IEnumerator
,所以它就像Iterable
Java 中的an 。相比之下,aStream
是一种迭代器,可与 an 相媲美,IEnumerator
因此声称这种数据类型可以在 .NET 中多次使用是错误的,对 的支持IEnumerator.Reset
是可选的。此处讨论的示例使用的事实是 anIEnumerable
可用于获取newIEnumerator
并且也适用于 Java 的Collection
s;你可以得到一个新的Stream
。如果 Java 开发人员决定直接添加Stream
操作Iterable
,中间操作返回另一个Iterable
,它确实具有可比性,并且可以以相同的方式工作。
However, the developers decided against it and the decision is discussed in this question. The biggest point is the confusion about eager Collection operations and lazy Stream operations. By looking at the .NET API, I (yes, personally) find it justified. While it looks reasonable looking at IEnumerable
alone, a particular Collection will have lots of methods manipulating the Collection directly and lots of methods returning a lazy IEnumerable
, while the particular nature of a method isn't always intuitively recognizable. The worst example I found (within the few minutes I looked at it) is List.Reverse()
whose name matches exactlythe name of the inherited (is this the right terminus for extension methods?) Enumerable.Reverse()
while having an entirely contradicting behavior.
但是,开发人员决定反对它,并在此问题中讨论了该决定。最大的一点是关于eager Collection 操作和lazy Stream 操作的混淆。通过查看 .NET API,我(是的,我个人)认为它是合理的。虽然IEnumerable
单独看看起来很合理,但一个特定的 Collection 将有很多方法直接操作 Collection 并且很多方法返回一个 lazy IEnumerable
,而方法的特殊性质并不总是可以直观地识别出来。我发现的最糟糕的例子(在我查看的几分钟内)是List.Reverse()
其名称与继承的名称完全匹配(这是扩展方法的正确终端吗?)Enumerable.Reverse()
同时具有完全矛盾的行为。
Of course, these are two distinct decisions. The first one to make Stream
a type distinct from Iterable
/Collection
and the second to make Stream
a kind of one time iterator rather than another kind of iterable. But these decision were made together and it might be the case that separating these two decision never was considered. It wasn't created with being comparable to .NET's in mind.
当然,这是两个截然不同的决定。第一个使Stream
类型与Iterable
/不同,Collection
第二个使Stream
类型成为一次性迭代器而不是另一种可迭代器。但是这些决定是一起做出的,可能从来没有考虑过将这两个决定分开。它的创建并不是为了与 .NET 相媲美。
The actual API design decision was to add an improved type of iterator, the Spliterator
. Spliterator
s can be provided by the old Iterable
s (which is the way how these were retrofitted) or entirely new implementations. Then, Stream
was added as a high-level front-end to the rather low level Spliterator
s. That's it. You may discuss about whether a different design would be better, but that's not productive, it won't change, given the way they are designed now.
实际的 API 设计决策是添加一种改进类型的迭代器,Spliterator
. Spliterator
s 可以由旧的Iterable
s(这是改造它们的方式)或全新的实现提供。然后,Stream
作为高级前端添加到相当低的级别Spliterator
s。就是这样。您可能会讨论不同的设计是否会更好,但这并没有成效,考虑到它们现在的设计方式,它不会改变。
There is another implementation aspect you have to consider. Stream
s are notimmutable data structures. Each intermediate operation may return a new Stream
instance encapsulating the old one but it may also manipulate its own instance instead and return itself (that doesn't preclude doing even both for the same operation). Commonly known examples are operations like parallel
or unordered
which do not add another step but manipulate the entire pipeline). Having such a mutable data structure and attempts to reuse (or even worse, using it multiple times at the same time) doesn't play well…
您必须考虑另一个实现方面。Stream
s不是不可变的数据结构。每个中间操作可能会返回一个Stream
封装旧实例的新实例,但它也可能操纵自己的实例并返回自身(这并不排除对同一操作执行两者)。众所周知的例子是像parallel
或unordered
这样的操作,它们不添加另一个步骤而是操纵整个管道)。拥有这样一个可变的数据结构并尝试重用(或者更糟的是,同时多次使用它)并不能很好地发挥作用……
For completeness, here is your quicksort example translated to the Java Stream
API. It shows that it does not really “take away much power”.
为了完整起见,这里是转换为 Java Stream
API 的快速排序示例。这表明它并没有真正“带走太多力量”。
static Stream<Integer> quickSort(Supplier<Stream<Integer>> ints) {
final Optional<Integer> optPivot = ints.get().findAny();
if(!optPivot.isPresent()) return Stream.empty();
final int pivot = optPivot.get();
Supplier<Stream<Integer>> lt = ()->ints.get().filter(i -> i < pivot);
Supplier<Stream<Integer>> gt = ()->ints.get().filter(i -> i > pivot);
return Stream.of(quickSort(lt), Stream.of(pivot), quickSort(gt)).flatMap(s->s);
}
It can be used like
它可以像
List<Integer> l=new Random().ints(100, 0, 1000).boxed().collect(Collectors.toList());
System.out.println(l);
System.out.println(quickSort(l::stream)
.map(Object::toString).collect(Collectors.joining(", ")));
You can write it even more compact as
你可以把它写得更紧凑
static Stream<Integer> quickSort(Supplier<Stream<Integer>> ints) {
return ints.get().findAny().map(pivot ->
Stream.of(
quickSort(()->ints.get().filter(i -> i < pivot)),
Stream.of(pivot),
quickSort(()->ints.get().filter(i -> i > pivot)))
.flatMap(s->s)).orElse(Stream.empty());
}
回答by Andrew Vermie
I think there are very few differences between the two when you look closely enough.
我认为当你仔细观察时,两者之间几乎没有区别。
At it's face, an IEnumerable
does appear to be a reusable construct:
从表面上看, anIEnumerable
似乎是一个可重用的构造:
IEnumerable<int> numbers = new int[] { 1, 2, 3, 4, 5 };
foreach (var n in numbers) {
Console.WriteLine(n);
}
However, the compiler is actually doing a little bit of work to help us out; it generates the following code:
然而,编译器实际上做了一些工作来帮助我们;它生成以下代码:
IEnumerable<int> numbers = new int[] { 1, 2, 3, 4, 5 };
IEnumerator<int> enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext()) {
Console.WriteLine(enumerator.Current);
}
Each time you would actually iterate over the enumerable, the compiler creates an enumerator. The enumerator is not reusable; further calls to MoveNext
will just return false, and there is no way to reset it to the beginning. If you want to iterate over the numbers again, you will need to create another enumerator instance.
每次实际迭代可枚举时,编译器都会创建一个枚举器。枚举器不可重用;进一步调用MoveNext
只会返回 false,并且无法将其重置为开头。如果要再次迭代数字,则需要创建另一个枚举器实例。
To better illustrate that the IEnumerable has (can have) the same 'feature' as a Java Stream, consider a enumerable whose source of the numbers is not a static collection. For example, we can create an enumerable object which generates a sequence of 5 random numbers:
为了更好地说明 IEnumerable 具有(可以具有)与 Java Stream 相同的“功能”,请考虑一个其数字源不是静态集合的可枚举。例如,我们可以创建一个可枚举对象,它生成 5 个随机数的序列:
class Generator : IEnumerator<int> {
Random _r;
int _current;
int _count = 0;
public Generator(Random r) {
_r = r;
}
public bool MoveNext() {
_current= _r.Next();
_count++;
return _count <= 5;
}
public int Current {
get { return _current; }
}
}
class RandomNumberStream : IEnumerable<int> {
Random _r = new Random();
public IEnumerator<int> GetEnumerator() {
return new Generator(_r);
}
public IEnumerator IEnumerable.GetEnumerator() {
return this.GetEnumerator();
}
}
Now we have very similar code to the previous array-based enumerable, but with a second iteration over numbers
:
现在我们的代码与之前的基于数组的可枚举非常相似,但在 上进行了第二次迭代numbers
:
IEnumerable<int> numbers = new RandomNumberStream();
foreach (var n in numbers) {
Console.WriteLine(n);
}
foreach (var n in numbers) {
Console.WriteLine(n);
}
The second time we iterate over numbers
we will get a different sequence of numbers, which isn't reusable in the same sense. Or, we could have written the RandomNumberStream
to thrown an exception if you try to iterate over it multiple times, making the enumerable actually unusable (like a Java Stream).
第二次迭代时,numbers
我们将得到不同的数字序列,这在同一意义上是不可重用的。或者,RandomNumberStream
如果您尝试多次迭代,我们可以编写to 抛出异常,使可枚举实际上无法使用(如 Java Stream)。
Also, what does your enumerable-based quick sort mean when applied to a RandomNumberStream
?
此外,当应用于 a 时,基于可枚举的快速排序意味着什么RandomNumberStream
?
Conclusion
结论
So, the biggest difference is that .NET allows you to reuse an IEnumerable
by implicitly creating a new IEnumerator
in the background whenever it would need to access elements in the sequence.
因此,最大的区别在于 .NET 允许您在需要访问序列中的元素时IEnumerable
通过IEnumerator
在后台隐式创建新元素来重用 an 。
This implicit behavior is often useful (and 'powerful' as you state), because we can repeatedly iterate over a collection.
这种隐式行为通常很有用(正如您所说的“强大”),因为我们可以反复迭代一个集合。
But sometimes, this implicit behavior can actually cause problems. If your data source is not static, or is costly to access (like a database or web site), then a lot of assumptions about IEnumerable
have to be discarded; reuse is not that straight-forward
但有时,这种隐含的行为实际上会导致问题。如果你的数据源不是静态的,或者访问成本很高(比如数据库或网站),那么很多关于的假设IEnumerable
必须被丢弃;重用不是那么简单
回答by John McClean
It is possible to bypass some of the "run once" protections in the Stream API; for example we can avoid java.lang.IllegalStateException
exceptions (with message "stream has already been operated upon or closed") by referencing and reusing the Spliterator
(rather than the Stream
directly).
可以绕过 Stream API 中的一些“运行一次”保护;例如,我们可以java.lang.IllegalStateException
通过引用和重用Spliterator
(而不是Stream
直接)来避免异常(带有消息“流已经被操作或关闭” )。
For example, this code will run without throwing an exception:
例如,此代码将运行而不会引发异常:
Spliterator<String> split = Stream.of("hello","world")
.map(s->"prefix-"+s)
.spliterator();
Stream<String> replayable1 = StreamSupport.stream(split,false);
Stream<String> replayable2 = StreamSupport.stream(split,false);
replayable1.forEach(System.out::println);
replayable2.forEach(System.out::println);
However the output will be limited to
但是输出将被限制为
prefix-hello
prefix-world
rather than repeating the output twice. This is because the ArraySpliterator
used as the Stream
source is stateful and stores its current position. When we replay this Stream
we start again at the end.
而不是重复输出两次。这是因为ArraySpliterator
用作Stream
源的 是有状态的并存储其当前位置。当我们重播时,我们会Stream
在最后重新开始。
We have a number of options to solve this challenge:
我们有多种选择来解决这一挑战:
We could make use of a stateless
Stream
creation method such asStream#generate()
. We would have to manage state externally in our own code and reset betweenStream
"replays":Spliterator<String> split = Stream.generate(this::nextValue) .map(s->"prefix-"+s) .spliterator(); Stream<String> replayable1 = StreamSupport.stream(split,false); Stream<String> replayable2 = StreamSupport.stream(split,false); replayable1.forEach(System.out::println); this.resetCounter(); replayable2.forEach(System.out::println);
Another (slightly better but not perfect) solution to this is to write our own
ArraySpliterator
(or similarStream
source) that includes some capacity to reset the current counter. If we were to use it to generate theStream
we could potentially replay them successfully.MyArraySpliterator<String> arraySplit = new MyArraySpliterator("hello","world"); Spliterator<String> split = StreamSupport.stream(arraySplit,false) .map(s->"prefix-"+s) .spliterator(); Stream<String> replayable1 = StreamSupport.stream(split,false); Stream<String> replayable2 = StreamSupport.stream(split,false); replayable1.forEach(System.out::println); arraySplit.reset(); replayable2.forEach(System.out::println);
The best solution to this problem (in my opinion) is to make a new copy of any stateful
Spliterator
s used in theStream
pipeline when new operators are invoked on theStream
. This is more complex and involved to implement, but if you don't mind using third party libraries, cyclops-reacthas aStream
implementation that does exactly this. (Disclosure: I am the lead developer for this project.)Stream<String> replayableStream = ReactiveSeq.of("hello","world") .map(s->"prefix-"+s); replayableStream.forEach(System.out::println); replayableStream.forEach(System.out::println);
我们可以使用无状态的
Stream
创建方法,例如Stream#generate()
. 我们必须在我们自己的代码中从外部管理状态并在Stream
“重播”之间重置:Spliterator<String> split = Stream.generate(this::nextValue) .map(s->"prefix-"+s) .spliterator(); Stream<String> replayable1 = StreamSupport.stream(split,false); Stream<String> replayable2 = StreamSupport.stream(split,false); replayable1.forEach(System.out::println); this.resetCounter(); replayable2.forEach(System.out::println);
另一个(稍微好点但不完美)的解决方案是编写我们自己的
ArraySpliterator
(或类似的Stream
源代码),其中包含一些重置当前计数器的能力。如果我们使用它来生成Stream
我们可能会成功地重播它们。MyArraySpliterator<String> arraySplit = new MyArraySpliterator("hello","world"); Spliterator<String> split = StreamSupport.stream(arraySplit,false) .map(s->"prefix-"+s) .spliterator(); Stream<String> replayable1 = StreamSupport.stream(split,false); Stream<String> replayable2 = StreamSupport.stream(split,false); replayable1.forEach(System.out::println); arraySplit.reset(); replayable2.forEach(System.out::println);
这个问题(在我看来)最好的解决办法是让任何有状态的新副本
Spliterator
中使用了sStream
管道时,新的运营商都在调用Stream
。这更复杂并且需要实现,但是如果您不介意使用第三方库,那么cyclops-react有一个Stream
完全可以做到这一点的实现。(披露:我是这个项目的首席开发人员。)Stream<String> replayableStream = ReactiveSeq.of("hello","world") .map(s->"prefix-"+s); replayableStream.forEach(System.out::println); replayableStream.forEach(System.out::println);
This will print
这将打印
prefix-hello
prefix-world
prefix-hello
prefix-world
as expected.
正如预期的那样。