【问题标题】:Java Stream API lazy evaluation internalsJava Stream API 惰性求值内部
【发布时间】:2021-05-20 03:09:46
【问题描述】:

我正在写一篇关于 Java Stream API 的文章。我已经阅读了 Stream 的整个包文档,并且在这里查看了类似的问题。

如果我这样说: “在终端操作被命中之前,不会评估流上的中间操作,这将实际执行它们,” 我会是正确的吗?我在 StackOverflow 上看到了不同的答案,而且每个中间操作都返回一个 Stream,所以我想知道它是否只是返回自己,然后只是跟踪中间操作以执行它。这就是“懒惰评估/执行”的意思吗?

下面的 javadoc 老实说给了我混合的信号。也许我只是愚蠢

中间操作返回一个新流。他们总是很懒惰; 执行诸如 filter() 之类的中间操作实际上并不 执行任何过滤,而是创建一个新流,当 遍历,包含初始流中匹配的元素 给定谓词。管道源的遍历直到 执行管道的终端操作。

非常感谢您的宝贵时间!抱歉,如果我遗漏了什么;(我已经坚持了很长时间!

P.S:Stream and lazy evaluation 非常相似,但是 “每个中间操作都会创建一个新流,存储提供的操作/函数并返回新流。”所以基本上,我的问题是,通过新流,它是否意味着与它相同的流?

【问题讨论】:

    标签: java java-stream


    【解决方案1】:

    所以我想知道它是不是自己返回了

    中间操作不返回给定的流。正如 JavaDoc 所说,它们返回一个 新流,即一个新对象。

    如果它是一个新对象,它如何知道之前在流上完成的操作?

    ...你可能会问。

    嗯,对象可以引用其他对象,对吧?即使您返回一个执行“映射”的新Stream,它仍然可以引用“旧流”(又名上游)。非常简单的例子:

    class StreamThatDoesMapping<U, T> implements Stream<T> {
        private Stream<U> upstream;
        private Function<? super U, ? extends T> mappingFunction;
    
        public StreamThatDoesMapping(Stream<U> upstream, Function<? super U, ? extends T> mappingFunction) {
            this.upstream = upstream;
            this.mappingFunction = mappingFunction;
        }
    
        // implementation details...
    }
    

    显然,在实现map 时,如果您随后将上游(this)传递给StreamThatDoesMapping,新流将“了解”您之前执行的操作!这就是您可以实现map 的方式,再次非常简化:

    public <R> Stream<R> map(Function<? super T, ? extends R> mapper) {
        return new StreamThatDoesMapping<>(this, mapper);
    }
    

    当您执行终端操作时,您是在“最下游”Stream 对象上执行此操作,该对象将从其上游获取元素,而该上游将从 its 上游获取元素, 等等。有点像链表,不是吗?

    请注意,这只是下游了解上游的一种方式。可能还有其他的,但这是ReferencePipeline 实现使用的。这是 JDK 中大多数 stream() 方法返回的内容。我强烈建议查看他们的源代码。

    【讨论】:

      【解决方案2】:

      如果我这样说:“直到终端操作被命中,才会对流上的中间操作进行评估,这将实际执行它们,”我是否正确?

      是的。

      我想知道它是否只是返回自己,然后只是跟踪中间操作以执行它。

      没有。

      它返回一个新的Stream 对象,该对象是根据先前的Stream 对象定义的。

      【讨论】:

      • 感谢您的快速回答,但我不太明白您所说的“就以前的 Stream 对象而言”是什么意思。
      • 假设Stream 上的唯一方法是forEachmap。然后你可以将Stream&lt;A&gt;.map(Function&lt;A, B&gt; f) 实现为return new Stream&lt;B&gt;() { @Override public void forEach(Consumer&lt;B&gt; consumer) { streamA.forEach(a -&gt; consumer.accept(f.apply(a))); } }。在调用forEach(终端操作)之前,它不会对streamA 做任何事情。新流没有自己的数据。
      • 啊,是的,那么编译器如何记住在流上实际执行的内容?这就是我得到的。非常感谢您的回答:D
      • 我上面描述的匿名类等价于class MappedStream&lt;A, B&gt; { private final Stream&lt;A&gt; streamA; private final Function&lt;A, B&gt; f; @Override public void forEach(...) { ... } }。 (您可以为这种类型填写一个构造函数。)想象另一个类,类似地实现,用于filter 和所有其他操作。所以它可能会变成new MappedStream(f, new FilteredStream(filter, ...(new CollectionStream(collection)..))
      猜你喜欢
      • 1970-01-01
      • 2014-02-28
      • 1970-01-01
      • 2011-07-14
      • 1970-01-01
      • 2012-08-31
      • 1970-01-01
      • 1970-01-01
      • 2011-02-23
      相关资源
      最近更新 更多