流程被神秘地消耗了两次

huangapple go评论79阅读模式
英文:

Stream mysteriously consumed twice

问题

以下是您提供的代码的翻译部分:

以下代码最终会出现java.lang.IllegalStateException: stream has already been operated upon or closed错误。

public static void main(String[] args) {
	Stream.concat(Stream.of("FOOBAR"),
			reverse(StreamSupport.stream(new File("FOO/BAR").toPath().spliterator(), true)
							.map(Path::toString)));
}

static <T> Stream<T> reverse(Stream<T> stream) {
	return stream.reduce(Stream.empty(),
			(Stream<T> a, T b) -> Stream.concat(Stream.of(b), a),
			(a, b) -> Stream.concat(b, a));
}

明显的解决方法是使用StreamSupport.stream(…, false)生成非并行流,但我无法理解为什么不能并行运行。

英文:

The following code ends up with a java.lang.IllegalStateException: stream has already been operated upon or closed.

public static void main(String[] args) {
	Stream.concat(Stream.of(&quot;FOOBAR&quot;),
			reverse(StreamSupport.stream(new File(&quot;FOO/BAR&quot;).toPath().spliterator(), true)
							.map(Path::toString)));
}

static &lt;T&gt; Stream&lt;T&gt; reverse(Stream&lt;T&gt; stream) {
	return stream.reduce(Stream.empty(),
			(Stream&lt;T&gt; a, T b) -&gt; Stream.concat(Stream.of(b), a),
			(a, b) -&gt; Stream.concat(b, a));
}

The obvious solution is to generate a non parallel stream with StreamSupport.stream(…, false), but I can’t see why can’t run in parallel.

答案1

得分: 4

Stream.empty()不是一个常量。这个方法在每次调用时都会返回一个新的流实例,就像任何其他流一样被消耗,例如当你将它传递给Stream.concat

因此,Stream.empty()不适合作为reduce标识值,因为标识值可能会被传递给约定意义模糊的规约函数任意次数的输入。这是一个实现细节,它恰好在串行规约时仅被使用一次,而在并行规约时可能被使用多次。

你可以使用:

static <T> Stream<T> reverse(Stream<T> stream) {
    return stream.map(Stream::of)
        .reduce((a, b) -> Stream.concat(b, a))
        .orElseGet(Stream::empty);
}

代替。

然而,我只将这个解决方案作为学术练习提供。一旦流变得很大,它会导致大量的concat调用,并且文档中的注释适用:

在构造重复连接的流时要小心。访问深度连接流的元素可能导致深层调用链,甚至可能导致StackOverflowError

一般情况下,当以这种方式使用流API时,生成的底层数据结构会比平面列表昂贵得多。

你可以使用类似这样的代码:

Stream<String> s = Stream.concat(Stream.of("FOOBAR"),
    reverse(new File("FOO/BAR").toPath()).map(Path::toString));
static Stream<Path> reverse(Path p) {
    ArrayDeque<Path> d = new ArrayDeque<>();
    p.forEach(d::addFirst);
    return d.stream();
}

或者

static Stream<Path> reverse(Path p) {
    Stream.Builder b = Stream.builder();
    for(; p != null; p = p.getParent()) b.add(p.getFileName());
    return b.build();
}

在Java 9+中,你可以使用一个真正没有额外存储的流(这不一定意味着它会更高效):

static Stream<Path> reverse(Path p) {
    return Stream.iterate(p, Objects::nonNull, Path::getParent).map(Path::getFileName);
}
英文:

Stream.empty() is not a constant. This method returns a new stream instance on each invocation that will get consumed like any other stream, e.g. when you pass it into Stream.concat.

Therefore, Stream.empty() is not suitable as identity value for reduce, as the identity value may get passed as input to the reduction function an arbitrary, intentionally unspecified number of times. It’s an implementation detail that is happens to be used only a single time for sequential reduction and potentially multiple times for parallel reduction.

You can use

static &lt;T&gt; Stream&lt;T&gt; reverse(Stream&lt;T&gt; stream) {
    return stream.map(Stream::of)
        .reduce((a, b) -&gt; Stream.concat(b, a))
        .orElseGet(Stream::empty);
}

instead.

However, I only provide the solution as an academic exercise. As soon as the stream gets large, it leads to an excessive amount of concat calls and the note of the documentation applies:

> Use caution when constructing streams from repeated concatenation. Accessing an element of a deeply concatenated stream can result in deep call chains, or even StackOverflowError.

Generally, the resulting underlying data structure will be far more expensive than a flat list, when using the Stream API this way.

You can use something like

Stream&lt;String&gt; s = Stream.concat(Stream.of(&quot;FOOBAR&quot;),
    reverse(new File(&quot;FOO/BAR&quot;).toPath()).map(Path::toString));
static Stream&lt;Path&gt; reverse(Path p) {
    ArrayDeque&lt;Path&gt; d = new ArrayDeque&lt;&gt;();
    p.forEach(d::addFirst);
    return d.stream();
}

or

static Stream&lt;Path&gt; reverse(Path p) {
    Stream.Builder b = Stream.builder();
    for(; p != null; p = p.getParent()) b.add(p.getFileName());
    return b.build();
}

With Java 9+ you can use a stream that truly has no additional storage (which does not necessarily imply that it will be more efficient):

static Stream&lt;Path&gt; reverse(Path p) {
    return Stream.iterate(p, Objects::nonNull, Path::getParent).map(Path::getFileName);
}

huangapple
  • 本文由 发表于 2020年10月16日 23:16:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/64391930.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定