为什么Java流是一次性的?

与c#的IEnumerable不同,在c#中,一个执行管道可以被我们想执行多少次就执行多少次,而在Java中,一个流只能被“迭代”一次。

任何对终端操作的调用都会关闭流,使其不可用。 这个“功能”会带走很多功能。< / p >

我想原因是技术。这种奇怪的限制背后的设计考虑是什么?

编辑:为了演示我所说的内容,请考虑以下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));
}

现在可以肯定的是,我并不主张这是一个快速排序的良好实现!然而,这是lambda表达式与流操作相结合的强大表达能力的一个很好的例子。

这在Java中是做不到的! 我甚至不能在不导致流不可用的情况下询问流是否为空。< / p >
44079 次浏览

背景

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

选择你的比较点-基本功能

使用基本概念,c#的IEnumerable概念与Java的Iterable更密切相关,后者能够创建尽可能多的迭代器IEnumerables创建IEnumerators。Java的Iterable创建Iterators

每个概念的历史是相似的,因为IEnumerableIterable都有一个基本动机,即允许在数据集合的成员上进行“for-each”风格的循环。这是一种过度简化,因为他们都允许更多的东西,而且他们也是通过不同的进程到达这个阶段的,但无论如何这是一个重要的共同特征。

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

选择您的比较点-增强功能

c#中的IEnumerable已被扩展以允许许多其他语言特性(主要与Linq有关)。添加的功能包括选择、投影、聚合等。这些扩展在集合论中有很强的使用动机,类似于SQL和关系数据库的概念。

Java 8还增加了一些功能,可以使用Streams和Lambdas进行一定程度的函数式编程。注意,Java 8流主要不是由集合论驱动的,而是由函数式编程驱动的。无论如何,两者有很多相似之处。

这是第二点。对c#的增强是作为IEnumerable概念的增强来实现的。然而,在Java中,所做的增强是通过创建Lambdas和Streams的新基本概念来实现的,然后还创建了一种相对简单的方法来将IteratorsIterables转换为Streams,反之亦然。

因此,比较IEnumerable和Java的Stream概念是不完整的。您需要将其与Java中的组合流和集合API进行比较。

在Java中,流与可迭代对象或迭代器不同

流不像迭代器那样被设计来解决问题:

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

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

使用Streams,您将一个函数序列链接在一起,然后向流提供一个输入值,并从组合的序列获得输出值。注意,在Java术语中,每个函数都封装在一个Stream实例中。Streams API允许你以连接转换表达式序列的方式链接Stream实例序列。

为了完成Stream概念,你需要一个数据源来提供流,以及一个使用流的终端函数。

向流中提供值的方式实际上可能来自Iterable,但Stream序列本身不是Iterable,它是一个复合函数。

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

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

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

c#的比较

当你认为Java流只是供应、流和收集系统的一部分,并且流和迭代器经常与集合一起使用时,那么就难怪很难将几乎全部嵌入到c#中的单个IEnumerable概念中的相同概念联系起来。

IEnumerable的某些部分(以及密切相关的概念)在所有Java Iterator、Iterable、Lambda和Stream概念中都很明显。

Java概念可以做的一些小事情在IEnumerable中比较困难,反之亦然。


结论

  • 这里没有设计问题,只是语言之间的概念匹配问题。
  • 流以不同的方式解决问题
  • 流为Java添加了功能(它们添加了一种不同的做事方式,而不是将功能带走)

添加流可以让你在解决问题时有更多选择,这可以被公平地归类为“增强力量”,而不是“减少”、“带走”或“限制”。

为什么Java流是一次性的?

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

不像c#的IEnumerable,一个执行管道可以被我们想执行多少次就执行多少次,在Java中一个流只能被“迭代”一次。

比较IEnumerableStream是错误的。你用来说IEnumerable可以被你想执行多少次就可以执行多少次的上下文,与Java Iterables相比是最好的,后者可以被你想迭代多少次。Java Stream表示IEnumerable概念的子集,而不是提供数据的子集,因此不能“重新运行”。

任何对终端操作的调用都会关闭流,使其不可用。这个“功能”会带走很多功能。

从某种意义上说,第一个说法是正确的。“剥夺权力”的说法则不然。你仍然在比较Streams和IEnumerables。流中的终端操作类似于for循环中的“break”子句。如果您愿意,并且可以重新提供所需的数据,您总是可以自由地拥有另一个流。同样,如果你认为IEnumerable更像一个Iterable,对于这个语句,Java做得很好。

我想这不是技术上的原因。这种奇怪的限制背后的设计考虑是什么?

原因是技术上的,原因很简单,流是思想的子集。流子集不控制数据供应,因此您应该重置供应,而不是流。在这种情况下,这并不奇怪。

快速排序的例子

您的快速排序示例具有签名:

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

你将输入IEnumerable作为数据源处理:

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

此外,返回值也是IEnumerable,这是一个数据的供应,由于这是一个排序操作,供应的顺序是重要的。如果你认为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));
}

注意有一个错误(我已经复制),在排序不处理重复值优雅,这是一个'唯一值'排序。

还要注意Java代码如何在不同的地方使用数据源(List)和流概念,而在c#中,这两个“个性”可以仅用IEnumerable表示。此外,虽然我已经使用List作为基类型,但我可以使用更通用的Collection,并且通过一个小的迭代器到流的转换,我可以使用更通用的Iterable

__abc0是围绕__abc1构建的,它们是有状态的可变对象。它们没有“重置”操作,事实上,要求支持这样的倒带操作会“消耗很多能量”。Random.ints()应该如何处理这样的请求?

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

这将由这种方法的作者来指定两次调用该方法意味着什么:它是否会像为未修改的数组或集合创建的流那样完全复制相同的序列,或者它是否会生成具有类似语义但元素不同的流,例如随机整数流或控制台输入行流,等等。


顺便说一下,为了避免混淆,终端操作消耗 the Stream关闭 the Stream不同,就像在流上调用close()那样(这对于具有关联资源的流来说是必需的,例如由Files.lines()产生)。


似乎许多混淆源于IEnumerableStream的误导性比较。IEnumerable表示能够提供实际的IEnumerator,因此它类似于Java中的Iterable。相比之下,Stream是一种迭代器,与IEnumerator类似,因此声称这种数据类型可以在。net中多次使用是错误的,对IEnumerator.Reset的支持是可选的。这里讨论的例子使用了这样一个事实:IEnumerable可以用来获取Stream5 IEnumerators,这也适用于Java的__abc10;你可以得到一个新的Stream。如果Java开发人员决定直接将Stream操作添加到Iterable,中间操作返回另一个Iterable,那么它确实具有可比性,并且可以以相同的方式工作。

然而,开发人员决定不这样做,这个决定在这个问题中讨论。最大的问题是对急切收集操作和惰性流操作的混淆。通过查看. net API,我(是的,个人)发现它是合理的。虽然单独查看IEnumerable看起来很合理,但特定的Collection将有许多方法直接操作该Collection,并且许多方法返回一个惰性IEnumerable,而方法的特定性质并不总是直观地可以识别。我发现的最糟糕的例子(在几分钟内我看了它)是List.Reverse(),它的名称与继承的完全的名称(这是扩展方法的正确终端吗?)Enumerable.Reverse()的名称匹配,同时具有完全矛盾的行为。


当然,这是两个截然不同的决定。第一个是使Stream成为不同于Iterable/Collection的类型,第二个是使Stream成为一种时间迭代器,而不是另一种迭代器。但这些决定是一起做出的,可能从来没有考虑过把这两个决定分开。它在创建时并没有考虑到与. net的可比性。

实际的API设计决定是添加一种改进的迭代器类型Spliterator。__abc0可以由旧的__abc2(这是它们被改造的方式)或全新的实现提供。然后,Stream作为高级前端添加到相当低级的__abc0中。就是这样。你可能会讨论不同的设计是否会更好,但这并没有什么成效,考虑到它们现在的设计方式,它不会改变。

还有另一个实现方面需要考虑。__abc0是不可变数据结构。每个中间操作都可能返回一个封装旧实例的新Stream实例,但它也可能操作自己的实例并返回自己(这并不排除对同一操作同时进行这两种操作)。常见的例子是像parallelunordered这样的操作,它们不添加另一个步骤,而是操作整个管道)。拥有这样一个可变的数据结构并试图重用(或者更糟糕的是,在同一时间多次使用它)并不会很好……


为了完整起见,下面是翻译为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);
}

它可以像这样使用

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(", ")));

你可以把它写得更紧凑

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

我认为当你仔细观察时,两者之间几乎没有什么区别。

从表面上看,IEnumerable确实是一个可重用的结构:

IEnumerable<int> numbers = new int[] { 1, 2, 3, 4, 5 };


foreach (var n in numbers) {
Console.WriteLine(n);
}

然而,编译器实际上做了一些工作来帮助我们;它生成以下代码:

IEnumerable<int> numbers = new int[] { 1, 2, 3, 4, 5 };


IEnumerator<int> enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext()) {
Console.WriteLine(enumerator.Current);
}

每次迭代枚举对象时,编译器都会创建一个枚举对象。枚举数不可重用;对MoveNext的进一步调用只会返回false,并且没有办法将其重置到开始。如果希望再次遍历这些数字,则需要创建另一个枚举器实例。


为了更好地说明IEnumerable具有(可以具有)与Java流相同的“特性”,可以考虑一个数字源不是静态集合的enumerable。例如,我们可以创建一个可枚举对象,它生成一个由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();
}
}

现在我们有了与前面基于数组的enumerable非常相似的代码,但是在numbers上进行了第二次迭代:

IEnumerable<int> numbers = new RandomNumberStream();


foreach (var n in numbers) {
Console.WriteLine(n);
}
foreach (var n in numbers) {
Console.WriteLine(n);
}

第二次遍历numbers时,我们将得到一个不同的数字序列,这在同一意义上不是可重用的。或者,我们可以编写RandomNumberStream来抛出异常,如果您尝试多次遍历它,使可枚举对象实际上不可用(就像Java流)。

同样,当应用于RandomNumberStream时,基于枚举的快速排序意味着什么?


结论

因此,最大的区别是。net允许你在需要访问序列中的元素时,在后台隐式地创建一个新的IEnumerator,从而重用IEnumerable

这种隐式行为通常是有用的(正如你所说的“强大”),因为我们可以重复迭代一个集合。

但有时,这种内隐行为实际上会导致问题。如果你的数据源不是静态的,或者访问成本很高(比如数据库或网站),那么很多关于IEnumerable的假设必须被丢弃;重用并不是那么简单

我有一些关于Streams API早期设计的回忆,可能会对设计原理有所启发。

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

我们需要确定的主要问题是API中链中的对象是什么样子的,以及它们如何连接到数据源。源通常是集合,但我们也希望支持来自文件或网络的数据,或动态生成的数据,例如,来自随机数生成器。

现有的工作对设计有很多影响。其中更有影响力的是谷歌的番石榴库和Scala集合库。(如果有人对Guava的影响感到惊讶,请注意凯文Bourrillion, Guava的主要开发者,是jsr - 335λ专家组的成员。)关于Scala集合,我们发现Martin Odersky的演讲特别有趣:面向未来的Scala集合:从可变到持久再到并行。(斯坦福EE380, 2011年6月1日。)

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

因为这些都是Iterables,所以你可以多次调用iterator()方法。那么会发生什么呢?

如果源是一个集合,这在大多数情况下工作正常。集合是可迭代的,每次调用iterator()都会产生一个独立于任何其他活动实例的Iterator实例,并且每次调用都独立地遍历集合。太好了。

现在,如果源是一次性的,比如从文件中读取行呢?也许第一个迭代器应该得到所有的值,但第二个和随后的迭代器应该为空。也许这些值应该在迭代器之间交错放置。或者每个Iterator都应该得到相同的值。那么,如果你有两个迭代器,其中一个比另一个走得更远怎么办?必须有人在第二个迭代器中缓冲这些值,直到它们被读取。更糟糕的是,如果你得到一个迭代器,读取所有的值,只有然后得到第二个迭代器。现在这些价值从何而来?是否有必要对它们都进行缓冲以防万一有人想要第二个迭代器?

显然,在一个一次性源上允许多个迭代器会引发很多问题。我们没有很好的答案。对于调用iterator()两次所发生的情况,我们希望有一致的、可预测的行为。这促使我们禁止多次遍历,使管道成为一次性的。

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

虽然DirectoryStream扩展了Iterable,但它不是一个通用的Iterable,因为它只支持一个迭代器;调用迭代器方法来获取第二个或后续迭代器会抛出IllegalStateException异常。

[原文加粗]

这看起来很不寻常,也很令人不愉快,以至于我们不想创建一大堆可能只有一次的新Iterables。这使得我们不再使用Iterable。

大约在这个时候,出现了一个文章作者:Bruce Eckel,描述了他在使用Scala时遇到的一些问题。他写了这样的代码:

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

这很简单。它将文本行解析为Registrant对象,并将它们打印两次。只不过它实际上只打印一次。事实证明,他认为registrants是一个集合,而实际上它是一个迭代器。对foreach的第二次调用遇到一个空迭代器,其中所有值都已耗尽,因此它什么也不打印。

这种经验使我们相信,如果尝试多次遍历,那么获得清晰可预测的结果是非常重要的。它还强调了区分惰性管道式结构与存储数据的实际集合的重要性。这反过来推动了将惰性管道操作分离到新的Stream接口中,并直接在Collections上只保留急切的、可变的操作。Brian Goetz解释道的基本原理。

如果允许对基于集合的管道进行多次遍历,而不允许对非基于集合的管道进行多次遍历,会怎么样呢?这是不一致的,但它是合理的。如果你正在从网络当然中读取值,你不能再次遍历它们。如果要多次遍历它们,则必须显式地将它们拉入一个集合。

但是让我们探索一下允许从基于集合的管道进行多次遍历。假设你是这样做的:

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

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

如果source是一个集合,那么第一个into()调用将创建一个返回源的iterator链,执行管道操作,并将结果发送到目标。第二次调用into()将创建另一个迭代器链,并执行管道操作再一次。这显然不是错误的,但它确实会对每个元素执行第二次所有筛选和映射操作。我认为许多程序员会对这种行为感到惊讶。

正如我上面提到的,我们一直在与Guava开发者进行交流。他们有一个很酷的东西是想法墓地,在那里他们描述了他们决定实现的特性以及原因。惰性集合的想法听起来很酷,但下面是他们对它的看法。考虑一个返回ListList.filter()操作:

这里最大的问题是太多的操作会变成昂贵的线性时间命题。如果你想过滤一个列表并返回一个列表,而不仅仅是一个Collection或Iterable,你可以使用ImmutableList.copyOf(Iterables.filter(list, predicate)),它“预先声明”它正在做什么以及它的代价有多高。

举个具体的例子,列表中get(0)size()的代价是多少?对于像ArrayList这样常用的类,它们是O(1)。但如果你在一个惰性过滤的列表中调用其中的一个,它必须在支持列表上运行过滤器,突然这些操作都是O(n)。更糟糕的是,它必须遍历每一个操作的备份列表。

这似乎是太多懒惰。设置一些操作并推迟实际执行直到你“开始”是一回事。另一种方法是隐藏大量的重新计算。

在提议禁止非线性或“不可重用”流时,保罗•山德士将允许它们的潜在的后果描述为导致“意外或令人困惑的结果”。他还提到并行执行会让事情变得更加棘手。最后,我还要补充一点,如果一个带有副作用的管道操作被意外地执行了多次,或者至少与程序员预期的次数不同,那么该操作将导致困难和模糊的错误。(但是Java程序员不会写带有副作用的lambda表达式,不是吗?不是吗? ?)

这就是Java 8 Streams API设计的基本原理,它允许一次遍历,并且需要一个严格的线性(无分支)管道。它提供了跨多个不同流源的一致行为,它清晰地将惰性操作与急切操作区分开来,并且它提供了一个简单的执行模型。


关于IEnumerable,我不是c#和. net的专家,所以如果我得出任何不正确的结论,我希望得到纠正(温和地)。然而,IEnumerable允许多重遍历以不同的方式处理不同的源;并且它允许嵌套IEnumerable操作的分支结构,这可能会导致一些重要的重新计算。虽然我知道不同的系统会做出不同的权衡,但这是我们在设计Java 8 Streams API时试图避免的两个特征。

OP给出的快速排序示例很有趣,令人费解,而且很抱歉地说,有些可怕。调用QuickSort接受一个IEnumerable并返回一个IEnumerable,因此在遍历最后的IEnumerable之前实际上没有进行排序。不过,这个调用所做的似乎是建立一个IEnumerables的树结构,它反映了快速排序所做的分区,而不是实际执行。(毕竟这是惰性计算。)如果源有N个元素,树最宽处有N个元素,深度为lg(N)层。

在我看来——再一次,我不是c#或。net专家——这将导致某些看起来无害的调用,例如通过ints.First()进行主元选择,比它们看起来更昂贵。当然,在第一级,它是O(1)但是考虑树的右边边缘的一个分区。要计算该分区的第一个元素,必须遍历整个源,这是一个O(N)操作。但是由于上面的分区是惰性的,它们必须重新计算,需要O(lg N)个比较。所以选择主元是一个O(nlgn)的操作,这和整个排序一样昂贵。

但实际上,在遍历返回的IEnumerable之前,我们并不进行排序。在标准的快速排序算法中,每一级分区都使分区的数量翻倍。每个分区只有一半大小,因此每个级别的复杂度保持在O(N)。分区树的高度是O(lgn)所以总的功是O(nlgn)

对于惰性IEnumerables树,树的底部有N个分区。计算每个分区需要遍历N个元素,每一个都需要lg(N)次比较。为了计算树底部的所有分区,需要O(N^2 lgn)次比较。

(对吗?我简直不敢相信。谁来帮我检查一下。)

在任何情况下,IEnumerable可以用这种方式来构建复杂的计算结构,这确实很酷。但是如果它确实像我认为的那样增加了计算的复杂性,那么这种编程方式似乎是应该避免的,除非你非常小心。

它可以绕过流API中的一些“运行一次”保护;例如,我们可以通过引用和重用Spliterator(而不是直接使用Stream)来避免java.lang.IllegalStateException异常(带有消息“流已被操作或关闭”)。

例如,这段代码将运行而不抛出异常:

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

然而,输出将限于

prefix-hello
prefix-world

而不是重复输出两次。这是因为用作Stream源的ArraySpliterator是有状态的,并存储其当前位置。当我们重放这个Stream时,我们从结尾重新开始。

我们有许多选择来解决这一挑战:

  1. 我们可以使用无状态的Stream创建方法,例如Stream#generate()。我们必须在自己的代码中从外部管理状态,并在Stream“replay”之间重置:

    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 Spliterators 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

像预期的那样。

原因是您可以从定义上只能使用一次的对象(如Iterator或BufferedReader)创建流。您可以将流视为使用BufferedReader读取文本文件到其末尾的方式。一旦您到达文件的末尾,BufferedReader不会停止存在,但它只是变得无用,因为您再也无法从中获得任何东西。如果要再次读取该文件,则必须创建一个新的读取器。对于流也是如此。如果希望对流的源进行两次处理,则必须创建两个单独的流。