为什么一次性Javastream?

与C#的IEnumerable ,在执行stream水线可以执行多次的情况下,Java中的stream只能被“迭代”一次。

任何对terminal操作的调用都会closuresstream,导致stream不可用。 这个“function”带走了很多权力。

我想这是不是技术性的原因。 这个奇怪的限制背后的devise考虑是什么?

编辑:为了演示我在说什么,请考虑以下在C#中的快速sorting实现:

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

可以肯定的是,我并不是主张这是一个快速sorting的好实现! 然而,这是lambdaexpression式与stream操作相结合的performance力的一个很好的例子。

而且不能用Java来完成! 我什至不能问一个stream是否是空的,而不会使其无法使用。

我从Streams API的早期devise中得到了一些回忆,可能会对devise原理有所了解。

早在2012年,我们就在语言中join了lambdaexpression式,我们想要一个面向集合的或“批量数据”的操作集,使用lambdas编程,这将促进并行性。 这一点懒惰地链接在一起的想法是很好的。 我们也不希望中间操作存储结果。

我们需要确定的主要问题是链中的对象在API中的样子以及它们如何连接到数据源。 源代码通常是集合,但我们也希望支持来自文件或networking的数据,或者例如从随机数生成器中即时生成的数据。

现有的工作对devise有很多影响。 其中更有影响力的是谷歌的番石榴图书馆和斯卡拉collections图书馆。 (如果有人对番石榴的影响感到惊讶,请注意,Guava首席开发人员Kevin Bourrillion是JSR-335 Lambda专家组的成员)。关于Scala的collections,我们发现Martin Odersky的这个演讲非常有趣: Future-校对Scala集合:从可变到持久到并行 。 (斯坦福EE380,2011年6月1日)

我们当时的原型devise基于Iterable 。 熟悉的操作filtermap等是Iterable上的扩展(默认)方法。 调用一个添加了一个操作链,并返回另一个Iterable 。 像count这样的terminal操作会将iterator()链调用到源,并且操作在每个阶段的迭代器中实现。

由于这些是Iterables,所以可以多次调用iterator()方法。 那么应该发生什么?

如果源是一个集合,这大多工作正常。 集合是可iterator() ,并且每次调用iterator()生成独立于任何其他活动实例的独立Iterator实例,并且每个独立遍历集合。 大。

现在如果源文件是一次性的,就像从文件中读取行一样? 也许第一个迭代器应该得到所有的值,但第二个和后续的应该是空的。 也许这些值应该在迭代器之间交错。 或者,也许每个迭代器应该得到所有相同的值。 那么,如果你有两个迭代器,而另一个又在另一个之前呢? 有人将不得不缓冲第二个迭代器中的值,直到他们被读取。 更糟糕的是,如果你得到一个Iterator并读取所有的值, 那么只有第二个Iterator。 价值从何而来? 是否有一个要求他们都被缓冲起来,以防万一有人想要第二个迭代器?

显然,允许多个迭代器通过一次性源提出了很多问题。 我们没有很好的答案。 我们想要一致的,可预测的行为,如果你调用iterator()两次会发生什么。 这促使我们不能进行多次遍历,使得pipe道一次性完成。

我们也观察到其他人碰到这些问题。 在JDK中,大多数Iterables是集合或集合类对象,允许多次遍历。 它没有在任何地方指定,但似乎有一个不成文的预期,即Iterables允许多次遍历。 NIO DirectoryStream接口是一个值得注意的例外。 其规范包括这个有趣的警告:

当DirectoryStream扩展Iterable时,它不是通用Iterable,因为它只支持一个Iterator; 调用迭代器方法来获得第二个或后续的迭代器抛出IllegalStateException。

[原文大胆]

这似乎是不寻常的,不愉快的,我们不想创造一大堆可能只有一次的新的无法计数的东西。 这使我们远离了使用Iterable。

大约在这个时候, Bruce Eckel的一篇文章出现了,描述了他与Scala碰到的一个麻烦。 他写了这个代码:

 // Scala val lines = fromString(data).getLines val registrants = lines.map(Registrant) registrants.foreach(println) registrants.foreach(println) 

这非常简单。 它将文本行分析为Registrant对象,并将其打印出来两次。 除了它实际上只打印一次。 事实certificate,他认为registrants是一个集合,实际上它是一个迭代器。 第二次调用foreach遇到一个空的迭代器,从中所有的值已经耗尽,所以它没有打印任何东西。

这种经验让我们相信,如果尝试多次遍历,获得明确可预见的结果是非常重要的。 它还强调了区分懒惰的类似于pipe道的结构与存储数据的实际集合的重要性。 这反过来又促使了惰性pipe道操作分离到新的Stream接口中,并直接在Collections上保持急切的,可变的操作。 布赖恩·戈茨(Brian Goetz)解释了这个理由。

那么对于基于集合的pipe道允许多次遍历,但是对于基于非收集的pipe道却不允许这样做呢? 这是不一致的,但它是明智的。 如果你从networking读取数值, 当然你不能再遍历它们。 如果你想遍历它们多次,你必须明确地将它们拖入一个集合。

但是我们来探讨一下允许多个遍历从基于集合的pipe道。 假设你这样做了:

 Iterable<?> it = source.filter(...).map(...).filter(...).map(...); it.into(dest1); it.into(dest2); 

into操作现在拼写collect(toList()) 。)

如果source是一个集合,那么第一个into()调用将创build一个迭代器链返回到源,执行pipe道操作,并将结果发送到目的地。 第二次调用into()将创build另一个Iterator链,并再次执行pipe道操作。 这并不是明显的错误,但它确实对每个元素都有第二次执行所有过滤和映射操作的效果。 我想很多程序员会对这种行为感到惊讶。

正如我上面提到的,我们一直在和番石榴开发者交谈。 他们所拥有的一件很酷的事情就是一个想法墓地 ,他们在那里描述他们决定实施原因的function。 懒惰集合的想法听起来很酷,但这是他们必须要说的。 考虑一个返回ListList.filter()操作:

这里最大的担忧是,太多的操作成为昂贵的线性时间命题。 如果你想要过滤一个列表并返回一个列表,而不仅仅是一个集合或者一个Iterable,你可以使用ImmutableList.copyOf(Iterables.filter(list, predicate)) ,它“事先陈述”它正在做什么以及如何它是昂贵的。

举一个具体的例子,列表中get(0)size()的成本是多less? 对于像ArrayList这样常用的类,它们是O(1)。 但是,如果你在一个懒惰的过滤列表中调用其中的一个,它必须在后备列表上运行filter,并且突然间这些操作是O(n)。 更糟的是,它必须遍历每个操作的支持列表。

我们觉得这懒惰了。 设置一些操作并推迟实际执行,直到你这么“走”是一回事。 另一种方式是设置隐藏重新计算的潜在的大量数据。

在提议不允许非线性或“不可重复使用”的stream程时, Paul Sandoz描述了允许它们产生“意想不到的或令人困惑的结果”的潜在后果 。 他还提到平行执行会使事情变得更加棘手。 最后,我想补充一点,如果操作意外地执行了多次,或者至less是程序员预期的不同次数,那么带有副作用的stream水线操作将会导致难以理解的和晦涩的错误。 (但Java程序员不写lambdaexpression式的副作用,是吗?他们吗?)

所以这就是Java 8 Streams APIdevise的基本原理,它允许一次遍历,并且需要一个严格的线性(无分支)pipe道。 它提供了跨多个不同stream源的一致行为,它清楚地区分了懒惰和渴望的操作,并提供了一个直接的执行模型。


关于IEnumerable ,我远不是C#和.NET的专家,所以如果我得出任何不正确的结论,我将不胜感激。 然而,它确实显示IEnumerable允许多遍历在不同的来源中performance不同。 它允许嵌套IEnumerable操作的分支结构,这可能会导致一些重要的重新计算。 虽然我明白不同的系统会做出不同的折衷,但这些是我们在deviseJava 8 Streams API时力求避免的两个特征。

OP给出的quicksort例子很有趣,令人费解,而且我很抱歉地说,有些可怕。 调用QuickSort需要一个IEnumerable并返回一个IEnumerable ,所以直到最终的IEnumerable被遍历,实际上没有sorting。 但是,这个调用似乎是build立了一个IEnumerables的树结构,它反映了quicksort可以做的分割,而没有真正做到。 (毕竟,这是懒惰的计算。)如果源有N个元素,那么这个树将是最宽的N个元素,并且它将是lg(N)级深度。

在我看来,再一次,我不是一个C#或.NET专家,这会导致一些无害的调用,比如通过ints.First()select透视选项比看起来更昂贵。 当然,第一级是O(1)。 但是考虑在树的深处,在右边。 要计算此分区的第一个元素,必须遍历整个源,即一个O(N)操作。 但是由于上面的分区是懒惰的,它们必须重新计算,需要O(lg N)比较。 所以select枢轴将是一个O(N lg N)的操作,这个操作和整个操作一样昂贵。

但是,直到我们遍历返回的IEnumerable我们才真正sorting。 在标准快速sortingalgorithm中,每个分区级别使分区数量加倍。 每个分区只有一半大小,所以每个分区都保持在O(N)的复杂度。 分区树是O(lg N)高,所以总的工作是O(N lg N)。

使用懒惰的IEnumerables树,在树的底部有N个分区。 计算每个分区需要遍历N个元素,每个元素都需要在树上进行lg(N)比较。 为了计算树底部的所有分区,需要进行O(N ^ 2 lg N)比较。

(这是对的吗?我很难相信,有人请检查一下。)

无论如何, IEnumerable可以用这种方式构build复杂的计算结构确实很酷。 但是,如果它确实增加了计算复杂度,就像我认为的那样,似乎这种编程是应该避免的,除非一个人非常小心。

背景

虽然这个问题看起来很简单,但实际的答案需要一些背景才有意义。 如果你想跳到结论,向下滚动…

select你的比较点 – 基本function

使用基本概念,C#的IEnumerable概念与Java的Iterable关系更密切,它能够根据需要创build尽可能多的迭代器 。 IEnumerables创buildIEnumerators 。 Java的Iterable创buildIterators

每个概念的历史都是相似的,因为IEnumerableIterable都有一个基本的动机来允许“for-each”风格循环遍历数据集合的成员。 这是一个过于简单化,因为他们都不仅仅是这样,而且他们也通过不同的进程来到这个阶段,但是无论如何这是一个重要的共同特征。

让我们来比较一下这个特性:在两种语言中,如果一个类实现了IEnumerable / Iterable ,那么这个类必须至less实现一个方法(对于C#,它是GetEnumerator ,Java是iterator() )。 在每种情况下,从该实例返回的实例( IEnumerator / Iterator )允许您访问当前和随后的数据成员。 此function用于for-each语言语法。

select你的比较点 – 增强的function

C#中的IEnumerable已经被扩展,以允许其他一些语言function( 主要与Linq有关 )。 添加的function包括select,预测,聚合等。这些扩展具有集合理论的强大动力,类似于SQL和关系数据库概念。

Java 8还添加了function,可以使用Streams和Lambdas实现一定程度的function性编程。 请注意,Java 8stream不主要受集合论的驱动,而是通过函数式编程。 无论如何,还有很多相似的地方。

所以,这是第二点。 对C#所做的改进是作为IEnumerable概念的增强来实现的。 然而在Java中,所做的改进是通过创buildLambdas和Streams的新基本概念来实现的,然后创build一种从IteratorsIterables转换为Streams的相对简单的方法,反之亦然。

所以,比较IEnumerable和Java的Stream概念是不完整的。 您需要将其与Java中的组合Streams和Collections API进行比较。

在Java中,Streams与Iterables或迭代器不一样

stream不是用来解决问题的,就像迭代器一样:

  • 迭代器是描述数据序列的一种方式。
  • stream是描述数据转换序列的一种方式。

用一个Iterator ,你得到一个数据值,处理它,然后得到另一个数据值。

通过Streams,您可以将一系列函数链接在一起,然后将input值提供给stream,并从组合的序列中获取输出值。 请注意,以Java术语来说,每个函数都封装在一个Stream实例中。 Streams API允许您以链接一系列转换expression式的方式链接一系列Stream实例。

为了完成Stream概念,您需要一个数据源来提供stream,以及一个消耗stream的terminal函数。

将值Iterablestream的方式实际上可以来自Iterable ,但Stream序列本身不是可Iterable ,它是一个复合函数。

Stream也被认为是懒惰的,从某种意义上说,它只有在你从中请求一个值的时候才能工作。

请注意Streams的这些重要假设和function:

  • Java中的Stream是一个转换引擎,它将一个数据项转换成另一个状态。
  • stream没有数据顺序或位置的概念,简单地转换他们所要求的任何东西。
  • stream可以提供来自许多来源的数据,包括其他stream,迭代器,Iterables,集合,
  • 你不能“重置”一个stream,就像“重新编程转换”。 重置数据源可能是你想要的。
  • 在逻辑上在stream中任何时候只有1个数据项在stream中(除非stream是并行stream,在这一点上,每个线程有1个项目)。 这与可能具有多于当前项目的“准备”被提供给stream的数据源或可能需要聚合和减less多个值的stream收集器无关。
  • stream可以是无限制的(无限),仅受数据源或收集器限制(也可以是无限的)。
  • stream是“可链接的”,过滤一个stream的输出是另一个stream。 input到stream并由其转换的值可以反过来被提供给执行不同转换的另一个stream。 处于转换状态的数据从一个stream到另一个stream。 您不需要介入并从一个stream中提取数据并将其插入到下一个stream中。

C#比较

如果您认为Java Stream只是供应,stream和收集系统的一部分,并且Streams和Iterator经常与Collections一起使用,那么难怪与相同的概念几乎所有embedded到C#中的单个IEnumerable概念。

在所有Java Iterator,Iterable,Lambda和Stream概念中,IEnumerable的一部分(以及相关的概念)都很明显。

Java概念可以做的小事情在IEnumerable中更难,反之亦然。


结论

  • 这里没有devise问题,只是在语言之间匹配概念的问题。
  • stream以不同的方式解决问题
  • stream向Java添加function(它们添加了一种不同的做事方式,它们不会去掉function)

在解决问题时,添加数据stream可以给你更多的select,这可以归类为“增强能力”,而不是“减less”,“带走”或“限制”。

为什么一次性Javastream?

这个问题是错误的,因为stream是函数序列,而不是数据。 根据提供数据stream的数据源,您可以重置数据源,并提供相同或不同的数据stream。

与C#的IEnumerable不同,在执行stream水线可以执行多次的情况下,Java中的stream只能被“迭代”一次。

比较IEnumerableStream是错误的。 您所使用的上下文IEnumerable可以随意多次执行,最好与Java Iterables相比,它可以随意多次迭代。 Java Stream表示IEnumerable概念的一个子集,而不是提供数据的子集,因此不能“重新运行”。

任何对terminal操作的调用都会closuresstream,导致stream不可用。 这个“function”带走了很多权力。

从某种意义上说,第一个陈述是真实的。 “拿走权力”声明不是。 你还在比较Streams IEnumerables。 stream中的terminal操作就像for循环中的“break”子句。 如果你愿意的话,你总是可以自由地拥有另一个stream,如果你可以重新提供你需要的数据。 同样,如果您认为IEnumerable更像是一个Iterable ,那么对于这个语句,Java就可以了。

我想这是不是技术性的原因。 这个奇怪的限制背后的devise考虑是什么?

原因是技术性的,并且出于一个简单的原因,一个Stream是它的一个子集。 stream子集不控制数据供应,所以您应该重置供应,而不是stream。 在这方面,这并不奇怪。

QuickSort示例

你的quicksort例子有签名:

 IEnumerable<int> QuickSort(IEnumerable<int> ints) 

您将input的IEnumerable视为数据源:

 IEnumerable<int> lt = ints.Where(i => i < pivot); 

此外,返回值也是IEnumerable ,它是一个数据的供应,由于这是一个Sort操作,所以这个供应的顺序很重要。 如果您认为Java Iterable类适合与此匹配,特别是IterableList专用化,因为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)); } 

请注意,有一个错误(我已经复制),因为sorting不处理重复值优雅,这是一个“独特的价值”sorting。

还要注意Java代码如何使用数据源( List )以及不同点的stream概念,而在C#中,这两个“个性”可以用IEnumerable表示。 另外,虽然我使用List作为基本types,但我可以使用更一般的Collection ,并且使用一个小的迭代器到Stream的转换,我可以使用更一般的Iterable

Spliterator是围绕Spliterator的,它们是有状态的,可变的对象。 他们没有“重新设置”的行动,事实上,要求支持这种倒退行动将“夺走许多权力”。 Random.ints()应该如何处理这样的请求?

另一方面,对于具有可回溯起源的Streams,很容易构造一个等效的Stream来再次使用。 只需将构buildStream的步骤放入可重用的方法即可。 请记住,重复这些步骤不是一个昂贵的操作,因为所有这些步骤都是惰性操作; 实际工作从terminal操作开始,根据实际的terminal操作,完全不同的代码可能会被执行。

作为这种方法的作者,你应该指定这个方法两次调用的含义:它是否重现与未修改的数组或集合所创build的stream完全相同的序列,还是会产生一个具有类似的语义,但不同的元素,如随机整数stream或控制台input行stream等。


顺便说一下,为了避免混淆,terminal操作消耗closures Stream截然不同的Stream就像在Stream上调用close()一样(对于具有关联资源(例如,由Files.lines()产生的Files.lines()所需的stream) 。


似乎很多混淆源于IEnumerableStream误导性比较。 一个IEnumerable代表提供一个实际的IEnumerator的能力,所以它像Java中的Iterable一样。 相比之下, Stream是一种迭代器,可以和IEnumerator相媲美,所以声称这种数据types可以在.NET中多次使用是错误的, IEnumerator.Reset的支持是可选的。 这里讨论的例子使用了一个事实, IEnumerable可以用来获取IEnumerable也可以用于Java的Collection 。 你可以得到一个新的Stream 。 如果Java开发人员决定直接将Stream操作添加到Iterable ,而中间操作返回另一个Iterable ,那么它真的是可比较的,并且可以以相同的方式工作。

然而,开发商决定反对这个问题 ,决定在这个问题上讨论。 最大的一点是关于渴望集合操作和懒惰stream操作的混淆。 通过查看.NET API,我(是的,亲自)发现它是合理的。 虽然看起来IEnumerable本身是合理的,但是一个特定的Collection将会有很多方法直接操纵Collection,并且很多方法返回一个懒惰的IEnumerable ,而方法的特殊性并不总是直观地被识别出来。 我发现的最糟糕的例子(在几分钟内,我看着它)是List.Reverse()其名称完全匹配inheritance的名称(这是扩展方法的正确终点?) Enumerable.Reverse()矛盾的行为。


当然,这是两个不同的决定。 第一个使Stream成为与Iterable / Collection不同的Iterable ,第二个使Stream成为一种一次迭代器而不是另一种迭代。 但是这些决定是一起做出来的,可能会把这两个决定分开考虑。 它不是创build与.NET相媲美的。

实际的APIdevise决定是添加一个改进的迭代器typesSpliteratorSpliterator可以由旧的Iterable (这是如何改进的方式)或全新的实现来提供。 然后, Stream被添加为相当低级别的Spliterator的高级Spliterator 。 而已。 你可能会讨论一个不同的devise是否会更好,但这不是生产性的,考虑到现在devise的方式,它不会改变。

还有另一个实现方面,你必须考虑。 数据Stream 不是不可变的数据结构。 每个中间操作可能会返回一个新的Stream实例封装旧的实例,但它也可能会操纵自己的实例,并返回自身(即使对同一操作也不排除)。 通常所知的例子是像parallelunordered操作,它们不会增加另一个步骤,而是操作整个stream水线)。 有这样一个可变的数据结构,并尝试重用(或更糟的是,同时使用它多次)不能很好地发挥…


为了完整起见,这里是将您的快速sorting示例转换为Java Stream API。 It shows that it does not really “take away much power”.

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

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:

 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.


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:

 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 :

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

Also, what does your enumerable-based quick sort mean when applied to a RandomNumberStream ?


结论

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.

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

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

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.

We have a number of options to solve this challenge:

  1. We could make use of a stateless Stream creation method such as Stream#generate() . We would have to manage state externally in our own code and reset between Stream "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); 
  2. Another (slightly better but not perfect) solution to this is to write our own ArraySpliterator (or similar Stream source) that includes some capacity to reset the current counter. If we were to use it to generate the Stream 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); 
  3. The best solution to this problem (in my opinion) is to make a new copy of any stateful Spliterator s used in the Stream pipeline when new operators are invoked on the Stream . This is more complex and involved to implement, but if you don't mind using third party libraries, cyclops-react has a Stream 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); 

This will print

 prefix-hello prefix-world prefix-hello prefix-world 

as expected.