不像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中是做不到的! 我甚至不能在不导致流不可用的情况下询问流是否为空。


当前回答

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

从表面上看,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非常相似的代码,但是对数字进行了第二次迭代:

IEnumerable<int> numbers = new RandomNumberStream();

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

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

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


结论

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

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

但有时,这种内隐行为实际上会导致问题。如果你的数据源不是静态的,或者访问成本很高(比如数据库或网站),那么很多关于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是有状态的,并且存储了它的当前位置。当我们重放这个流时,我们会在最后重新开始。

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

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

这将打印

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

像预期的那样。

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

另一方面,对于具有可追溯起源的流,很容易构造一个等价的流来再次使用。只需将构造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 new IEnumerators and that works with Java’s Collections 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.

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 exactly the name of the inherited (is this the right terminus for extension methods?) Enumerable.Reverse() while having an entirely contradicting behavior.


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

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

There is another implementation aspect you have to consider. Streams are not immutable 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…


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

The reason is that you can create streams from things that can only be used once by definition, such as an Iterator or a BufferedReader. You can think of a Stream as being consumed the same way as having used a BufferedReader to read a text file to its end. Once you reach the end of the file, the BufferedReader doesn't stop existing, but it just become useless as you can't get anything out of it anymore. If you want to read the file again, you have to create a new reader. The same goes for streams. If you want to process the source of the stream twice, you have to create two separate streams.

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

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

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

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

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

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

如果源是一个集合,这在大多数情况下工作正常。集合是可迭代的,每次调用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 then get a second Iterator. Where do the values come from now? Is there a requirement for them all to be buffered up just in case somebody wants a second 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开发者进行交流。他们有一个很酷的东西是Idea Graveyard,在那里他们描述了他们决定不实现的功能以及原因。惰性集合的想法听起来很酷,但下面是他们对它的看法。考虑一个返回List的List.filter()操作:

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

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

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

In proposing to disallow non-linear or "no-reuse" streams, Paul Sandoz described the potential consequences of 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??)

这就是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.

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.

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.

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

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

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

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

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

从表面上看,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非常相似的代码,但是对数字进行了第二次迭代:

IEnumerable<int> numbers = new RandomNumberStream();

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

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

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


结论

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

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

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