与C#的IEnumerable
不同,其中执行管道可以根据需要执行多次,在Java中,流只能“迭代”一次。
对终端操作的任何调用都会关闭流,使其无法使用。 这个“功能”消耗了很多力量。
我想这是不技术的原因。这个奇怪的限制背后的设计考虑是什么?
编辑:为了演示我在说什么,请考虑以下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完成! 我甚至无法询问流是否为空而不使其无法使用。
答案 0 :(得分:350)
我对Streams API的早期设计有一些回忆,可能会对设计原理有所了解。
早在2012年,我们在语言中添加了lambda,我们想要一个面向集合的数据&#34;批量数据&#34;使用lambda编程的一组操作,这将促进并行性。在这一点上,懒洋洋地将操作链接在一起的想法已经确立。我们也不希望中间操作存储结果。
我们需要确定的主要问题是链中的对象在API中的样子以及它们如何连接到数据源。源通常是集合,但我们也希望支持来自文件或网络的数据,或者即时生成的数据,例如,来自随机数生成器。
现有工作对设计有很多影响。其中影响力最大的是Google的Guava图书馆和Scala馆藏图书馆。 (如果有人对Guava的影响感到惊讶,请注意Kevin Bourrillion,Guava首席开发人员,在JSR-335 Lambda专家组。)在Scala集合中,我们发现Martin Odersky的这个演讲特别具体兴趣:Future-Proofing Scala Collections: from Mutable to Persistent to Parallel。 (Stanford EE380,2011年6月1日。)
我们当时的原型设计基于Iterable
。熟悉的操作filter
,map
等是Iterable
上的扩展(默认)方法。调用一个操作向链添加了一个操作并返回另一个Iterable
。像count
这样的终端操作会将链条上的iterator()
调用到源,并且操作在每个阶段的迭代器中实现。
由于这些是Iterables,因此您可以多次调用iterator()
方法。那会发生什么呢?
如果源是一个集合,这大多数工作正常。集合是可迭代的,每次调用iterator()
都会生成一个独立于任何其他活动实例的独特Iterator实例,并且每个实例都独立遍历集合。大。
现在如果源是一次性的,比如从文件中读取行,该怎么办?也许第一个迭代器应该获得所有值,但第二个和后续迭代器应该是空的。也许值应该在迭代器之间交错。或者也许每个迭代器都应该得到所有相同的值。那么,如果你有两个迭代器而另一个比另一个更远呢?有人必须缓冲第二个迭代器中的值,直到它们被读取。更糟糕的是,如果您获得一个迭代器并读取所有值,并且只有然后获得第二个迭代器,该怎么办?这些价值从何而来?有没有要求所有人都被缓存以防万一有人想要第二个迭代器?
显然,在一次性源上允许多个迭代器会引发很多问题。我们没有为他们提供好的答案。如果您两次调用iterator()
,我们需要一致,可预测的行为。这促使我们不允许多次遍历,使管道一炮而红。
我们也观察到其他人遇到了这些问题。在JDK中,大多数Iterables是集合或类似集合的对象,它们允许多次遍历。它没有在任何地方指定,但似乎有一个不成文的期望Iterables允许多次遍历。一个值得注意的例外是NIO DirectoryStream接口。它的规范包括这个有趣的警告:
虽然DirectoryStream扩展了Iterable,但它不是通用的Iterable,因为它只支持一个Iterator;调用iterator方法获取第二个或后续迭代器会抛出IllegalStateException。
[原件加粗]
这看起来很不寻常和令人不快,我们并不想创造一大堆可能只有一次的新Iterables。这促使我们远离使用Iterable。
大约在这个时候,article by Bruce Eckel似乎描述了他与Scala有关的麻烦。他写了这段代码:
// Scala
val lines = fromString(data).getLines
val registrants = lines.map(Registrant)
registrants.foreach(println)
registrants.foreach(println)
这很简单。它将文本行解析为Registrant
个对象,并将它们打印出来两次。除了它实际上只打印出一次。事实证明,他认为registrants
是一个集合,而实际上它是一个迭代器。对foreach
的第二次调用遇到一个空迭代器,所有值都已耗尽,因此它不会打印任何内容。
这种经历使我们确信,如果尝试多次遍历,那么获得明确可预测的结果非常重要。它还强调了区分类似管道的惰性结构与存储数据的实际集合的重要性。这反过来又将延迟管道操作分离到新的Stream接口,并且只在集合上直接保留急切的变异操作。 Brian Goetz has explained这个理由。
如何允许对基于集合的管道进行多次遍历,但不允许对非基于集合的管道进行遍历?这是不一致的,但这是明智的。如果您正在从网络中读取值,当然,则无法再次遍历它们。如果要多次遍历它们,则必须明确地将它们拖入集合中。
但是,让我们探讨允许从基于集合的管道进行多次遍历。我们说你这样做了:
Iterable<?> it = source.filter(...).map(...).filter(...).map(...);
it.into(dest1);
it.into(dest2);
(into
操作现在拼写为collect(toList())
。)
如果source是一个集合,那么第一个into()
调用将创建一个迭代链返回源,执行管道操作,并将结果发送到目标。对into()
的第二次调用将创建另一个迭代器链,并再次执行的管道操作。这显然不是错误的,但它确实具有为每个元素第二次执行所有过滤器和映射操作的效果。我认为很多程序员会对这种行为感到惊讶。
正如我上面提到的,我们一直在与Guava开发人员交谈。他们有一个很酷的事情是Idea Graveyard,他们在这里描述了他们决定不实现的功能以及原因。懒惰收藏的想法听起来很酷,但这里有他们要说的。考虑一个返回List.filter()
的{{1}}操作:
这里最大的担忧是太多的操作成为昂贵的线性时间命题。如果您想过滤列表并获取列表,而不仅仅是集合或Iterable,您可以使用
List
,其中&#34;预先声明&#34;它做了什么以及它有多贵。
举一个具体的例子,列表中ImmutableList.copyOf(Iterables.filter(list, predicate))
或get(0)
的费用是多少?对于像size()
这样常用的类,它们是O(1)。但是如果你在一个延迟过滤的列表中调用其中一个,它必须在后备列表上运行过滤器,并且突然这些操作是O(n)。更糟糕的是,它必须遍历每次操作的后备列表。
这对我们来说似乎是太多懒惰。设置一些操作并推迟实际执行是一回事,直到你这样做为止&#34; Go&#34;。另一种方法就是以可能隐藏大量重新计算的方式进行设置。
建议禁止非线性或“不再使用”#34;溪流,Paul Sandoz描述了potential consequences允许它们引起意外或混乱的结果。&#34;他还提到并行执行会使事情更棘手。最后,我补充说,如果操作意外地执行了多次,或者至少与程序员预期的次数不同,那么带有副作用的管道操作会导致困难和模糊的错误。 (但是Java程序员不会编写带有副作用的lambda表达式,是吗?做他们吗?)
这就是Java 8 Streams API设计的基本原理,它允许一次性遍历,并且需要严格的线性(无分支)管道。它提供跨多个不同流源的一致行为,它清楚地区分了懒惰和急切操作,并提供了一种简单的执行模型。
关于ArrayList
,我远不是C#和.NET的专家,所以如果我得出任何不正确的结论,我将不胜感激(温和地)。但是,似乎IEnumerable
允许多个遍历在不同的源上表现不同;并且它允许嵌套IEnumerable
操作的分支结构,这可能导致一些重要的重新计算。虽然我理解不同的系统做出不同的权衡,但这些是我们在设计Java 8 Streams API时要避免的两个特征。
OP给出的快速举例很有意思,令人费解,我很遗憾地说,有点可怕。调用IEnumerable
需要QuickSort
并返回IEnumerable
,因此在遍历最终IEnumerable
之前,实际上不会进行排序。但是,调用似乎要做的是建立一个IEnumerable
的树结构,它反映了quicksort将要执行的分区,而不是实际执行它。 (毕竟这是懒惰的计算。)如果源有N个元素,那么树的最宽处将是N个元素,并且它将是lg(N)级别。
在我看来 - 再一次,我不是C#或.NET专家 - 这将导致某些看似无害的调用,例如通过IEnumerables
进行数据透视选择比他们看起来更贵。当然,在第一级,它是O(1)。但是考虑在树的深处,在右边缘。要计算此分区的第一个元素,必须遍历整个源,即O(N)操作。但由于上面的分区是惰性的,因此必须重新计算它们,需要进行O(lg N)比较。因此,选择枢轴将是O(N lg N)操作,这与整个操作一样昂贵。
但是在我们遍历返回的ints.First()
之前,我们实际上并没有排序。在标准快速排序算法中,每个分区级别使分区数量加倍。每个分区只有一半大小,因此每个级别都保持O(N)复杂度。分区树的高度为O(lg N),因此总工作量为O(N lg N)。
使用懒惰的IEnumerables树,在树的底部有N个分区。计算每个分区需要遍历N个元素,每个元素都需要在树上进行lg(N)比较。要计算树底部的所有分区,则需要进行O(N ^ 2 lg N)比较。
(这是对的吗?我几乎不敢相信。有人请为我检查一下。)
在任何情况下,IEnumerable
都可以用这种方式构建复杂的计算结构,这确实很酷。但是,如果它确实增加了我认为的计算复杂度,那么除非一个人非常小心,否则应该避免这种编程方式。
答案 1 :(得分:120)
虽然问题看似简单,但实际答案需要一些背景才有意义。如果您想跳到结论,请向下滚动...
使用基本概念,C#的IEnumerable
概念与Java's Iterable
更密切相关,Iterators能够根据需要创建尽可能多的IEnumerables
。 IEnumerators
创建mostly related to Linq。 Java Iterable
创建Iterators
每个概念的历史都是相似的,因为IEnumerable
和Iterable
都有一个基本的动机,即允许每个概念。样式循环遍历数据集合的成员。这是一个过于简单化的原因,因为它们不仅仅允许这样,而且它们也通过不同的进展来到那个阶段,但无论如何它都是一个重要的共同特征。
让我们比较一下这个特性:在两种语言中,如果一个类实现IEnumerable
/ Iterable
,那么该类必须至少实现一个方法(对于C#,它&#39) ; s GetEnumerator
和Java iterator()
)。在每种情况下,从(IEnumerator
/ Iterator
)返回的实例允许您访问数据的当前和后续成员。此功能用于for-each语言语法。
IEnumerable
已扩展为允许许多其他语言功能({{3}})。添加的功能包括选择,投影,聚合等。这些扩展在集合理论中的使用具有很强的动机,类似于SQL和关系数据库概念。
Java 8还添加了一些功能,以便使用Streams和Lambdas实现一定程度的功能编程。请注意,Java 8流不是主要由集合论推动,而是通过函数式编程。无论如何,有很多相似之处。
所以,这是第二点。对C#的增强实现为IEnumerable
概念的增强。但是,在Java中,所做的增强是通过创建Lambdas和Streams的新基本概念来实现的,然后还创建了一种从Iterators
和Iterables
转换为Streams的相对简单的方法,反之亦然。
因此,将IEnumerable与Java的Stream概念进行比较是不完整的。您需要将它与Java中的组合Streams和Collections API进行比较。
Streams并非旨在以与迭代器相同的方式解决问题:
使用Iterator
,您将获得一个数据值,对其进行处理,然后获取另一个数据值。
使用Streams,您可以将一系列函数链接在一起,然后将输入值提供给流,并从组合序列中获取输出值。注意,在Java术语中,每个函数都封装在一个Stream
实例中。 Streams API允许您以链接一系列转换表达式的方式链接一系列Stream
实例。
为了完成Stream
概念,您需要一个数据源来提供流,以及一个消耗流的终端函数。
您向流中提供值的方式实际上可能来自Iterable
,但Stream
序列本身不是Iterable
,它是复合函数。
Stream
也是懒惰的,因为只有当你从中请求一个值时它才有用。
请注意Streams的这些重要假设和功能:
Stream
是一个转换引擎,它将一个状态的数据项转换为另一个状态。如果您认为Java Stream只是供应,流和收集系统的一部分,并且Streams和Iterators通常与Collections一起使用,那么难以与它相关联概念几乎全部嵌入到C#中的单个IEnumerable
概念中。
IEnumerable(以及密切相关的概念)的部分内容在所有Java Iterator,Iterable,Lambda和Stream概念中都很明显。
Java概念可以做的小事情在IEnumerable中更难,反之亦然。
添加流可以在解决问题时为您提供更多选择,这可以归类为“增强权力”,而不是“减少”,“带走”,或者&#39;限制&#39;它
这个问题是错误的,因为流是功能序列,而不是数据。根据提供流的数据源,您可以重置数据源,并提供相同或不同的流。
将IEnumerable
与Stream
进行比较是错误的。您用来表示IEnumerable
的上下文可以根据需要执行多次,最好与Java Iterables
进行比较,后者可以根据需要进行多次迭代。 Java Stream
代表IEnumerable
概念的子集,而不是提供数据的子集,因此无法重新运行。
从某种意义上说,第一种说法是正确的。 &#39;带走权力&#39;声明不是。你还在比较它的IEnumerables。流中的终端操作就像一个“突破”。 for循环中的子句。如果需要,您可以随时拥有另一个流,并且可以重新提供所需的数据。同样,如果你认为IEnumerable
更像Iterable
,对于这个语句,Java就可以了。
原因是技术性的,原因很简单,Stream是认为它的一部分。流子集不控制数据供应,因此您应该重置供应,而不是流。在这种情况下,它并不那么奇怪。
您的快速排序示例有签名:
IEnumerable<int> QuickSort(IEnumerable<int> ints)
您将输入IEnumerable
视为数据源:
IEnumerable<int> lt = ints.Where(i => i < pivot);
此外,返回值也是IEnumerable
,这是一个数据供应,由于这是一个排序操作,因此该供应的顺序很重要。如果您认为Java Iterable
类是适当的匹配,特别是List
的{{1}}特化,因为List是一个具有保证顺序或迭代的数据源,那么代码的等效Java代码是:
Iterable
注意有一个错误(我已经复制了),因为排序不能优雅地处理重复值,它是一个独特的值&#39;排序
另请注意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));
}
),以及在不同时刻流式传输概念,以及在C#中使用数据源&#39;可以用List
表示。此外,虽然我使用IEnumerable
作为基本类型,但我可以使用更通用的List
,并且使用更小的迭代器到流转换,我可以使用更通用的{{{ 1}}
答案 2 :(得分:20)
Stream
是围绕Spliterator
构建的,它们是有状态的可变对象。他们没有“重置”动作,事实上,要求支持这种倒带动作会“带走很多力量”。 Random.ints()
如何处理这样的请求?
另一方面,对于具有可追溯原点的Stream
s,很容易构造一个等效的Stream
来再次使用。只需将构建Stream
的步骤放入可重用的方法中。请记住,重复这些步骤并不是一项昂贵的操作,因为所有这些步骤都是惰性操作;实际工作从终端操作开始,根据实际的终端操作,可能会执行完全不同的代码。
这样一个方法的作者,你可以指定调用该方法两次暗示的内容:它是否重现完全相同的序列,为未修改的数组或集合创建的流,或者它是否产生了具有类似语义但不同元素的流,如随机整数流或控制台输入行流等。
顺便说一句,为了避免混淆,终端操作消耗与{em>关闭 Stream
不同的Stream
作为调用{{流上有1}}(这是具有相关资源的流所必需的,例如由close()
生成的)。
似乎很多混淆源于误导Files.lines()
与IEnumerable
的比较。 Stream
表示提供实际IEnumerable
的能力,因此它类似于Java中的IEnumerator
。相比之下,Iterable
是一种迭代器并且与Stream
相当,所以声称这种数据类型可以在.NET中多次使用是错误的,对{{1}的支持是错误的是可选的。这里讨论的示例使用的事实是IEnumerator
可用于获取 new IEnumerator.Reset
,并且也适用于Java的IEnumerable
;你可以得到一个新的IEnumerator
。如果Java开发人员决定直接将Collection
操作添加到Stream
,并且中间操作返回另一个Stream
,那么它实际上是可比较的,它可以以相同的方式工作。
但是,开发人员决定反对它,并在this question中讨论了该决定。最重要的一点是关于急切的Collection操作和懒惰的Stream操作的混乱。通过查看.NET API,我(是的,亲自)认为它是合理的。虽然单独看Iterable
看起来很合理,但是一个特定的Collection会有很多方法直接操作Collection,而很多方法返回一个懒惰的Iterable
,而方法的特殊性质并不总是直观的识别。我发现的最糟糕的例子(在我查看它的几分钟内)是List.Reverse()
,其名称与完全继承的名称相匹配(这是扩展方法的正确终点吗?){{ 3}}虽然有完全矛盾的行为。
当然,这是两个截然不同的决定。第一个使IEnumerable
类型与IEnumerable
/ Stream
不同,第二个使Iterable
成为一种迭代器,而不是另一种迭代。但是这些决定是在一起进行的,可能是因为分离这两个决定从未被考虑过。它不是为了与.NET的可比性而创建的。
实际的API设计决策是添加一种改进的迭代器类型Collection
。 Stream
可以由旧的Spliterator
提供(这是改进的方式)或全新的实现。然后,Spliterator
被添加为相当低级Iterable
的高级前端。而已。您可以讨论一个不同的设计是否会更好,但这不是很有效,它不会改变,因为它们现在的设计方式。
您必须考虑另一个实施方面。 Stream
是不不可变数据结构。每个中间操作可以返回一个封装旧实例的新Spliterator
实例,但它也可以操纵它自己的实例并返回自身(这并不妨碍对同一操作同时执行)。众所周知的例子是像Stream
或Stream
这样的操作,它们不会添加另一个步骤而是操纵整个管道。拥有这样一个可变数据结构并尝试重用(或者更糟糕的是,同时多次使用它)并不能很好地发挥作用......
为了完整性,这里将您的快速排序示例翻译为Java parallel
API。它表明它并没有真正“消耗太多力量”。
unordered
可以像
一样使用Stream
你可以把它写成更紧凑的
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);
}
答案 3 :(得分:8)
我认为当你仔细观察时,两者之间的差异很小。
从表面看,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具有(可以拥有)相同的功能&#39;作为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();
}
}
现在我们的代码与之前基于数组的可枚举代码非常相似,但第二次迭代超过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 Stream)。
此外,基于可枚举的快速排序在应用于RandomNumberStream
时意味着什么?
因此,最大的区别在于.NET允许您通过在后台隐式创建新IEnumerable
来重用IEnumerator
,只要它需要访问序列中的元素。
这种隐含的行为通常很有用(并且在您声明的情况下具有强大的功能),因为我们可以反复迭代集合。
但有时,这种隐性行为实际上可能会导致问题。如果您的数据源不是静态的,或者访问成本很高(如数据库或网站),则必须丢弃许多关于IEnumerable
的假设;重用不是那么直接
答案 4 :(得分:1)
可以绕过一些&#34;运行一次&#34; Stream API中的保护;例如,我们可以通过引用和重用java.lang.IllegalStateException
(而不是Spliterator
来避免Stream
例外(使用消息&#34;流已经被操作或关闭&#34;) )。
例如,此代码将在不抛出异常的情况下运行:
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
而不是重复输出两次。这是因为用作ArraySpliterator
源的Stream
是有状态的并存储其当前位置。当我们重播这个Stream
时,我们会在最后重新开始。
我们有很多选择来解决这个挑战:
我们可以使用无状态Stream
创建方法,例如Stream#generate()
。我们必须在我们自己的代码中外部管理状态,并在Stream
&#34;重播&#34;:
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
管道中调用新运算符时为Stream
管道创建的任何有状态Stream
的新副本1}}。这更加复杂并且需要实施,但如果您不介意使用第三方库,cyclops-react具有Stream
实现,可以完成此操作。 (披露:我是这个项目的首席开发人员。)
Stream<String> replayableStream = ReactiveSeq.of("hello","world")
.map(s->"prefix-"+s);
replayableStream.forEach(System.out::println);
replayableStream.forEach(System.out::println);
这将打印
prefix-hello
prefix-world
prefix-hello
prefix-world
正如所料。
答案 5 :(得分:0)
原因是您可以从定义只能使用一次的事物创建流,例如 Iterator 或 BufferedReader。您可以认为 Stream 的使用方式与使用 BufferedReader 将文本文件读取到最后的方式相同。一旦到达文件末尾,BufferedReader 就不会停止存在,但它会变得毫无用处,因为您再也无法从中获取任何信息。如果要再次读取文件,则必须创建一个新的阅读器。流也是如此。如果要对流的源进行两次处理,则必须创建两个单独的流。