在哪些情况下Stream操作应该是有状态的?

时间:2015-10-10 20:30:08

标签: java java-8 java-stream

javaodoc for the stream packageParallelism部分的末尾,我读到了:

  

大多数流操作接受描述用户指定行为的参数,这些参数通常是lambda表达式。为了保持正确的行为,这些行为参数必须是非干扰的,,并且在大多数情况下必须是无状态的

我很难理解这个“在大多数情况下”。在哪些情况下可以接受/希望进行有状态流操作?

我的意思是,我知道这是可能的,特别是在使用顺序流时,但同样的javadoc明确指出:

  

除了标识为明确不确定的操作(例如findAny())之外,流是顺序执行还是并行执行不应更改计算结果。

还有:

  

另请注意,尝试从行为参数访问可变状态会使您在安全性和性能方面做出错误选择; [...]最好的方法是避免有状态的行为参数完全流动操作;通常有一种方法可以重构流管道以避免有状态。

所以,我的问题是:在哪种情况下使用有状态流操作是一种好习惯(而不是用于副作用的方法,例如forEach)?

相关问题可能是:为什么有副作用的操作,例如forEach?我总是做一个好的旧for循环,以避免在我的lambda表达式中产生副作用。

4 个答案:

答案 0 :(得分:2)

有状态流lambda的示例:

  • collect(Collector)Collector按定义是有状态的,因为它必须收集集合(状态)中的所有元素。
  • forEach(Consumer):根据定义,Consumer是有状态的,除非它是黑洞(无操作)。
  • peek(Consumer):根据定义,Consumer是有状态的,因为如果不将其存储在某处(例如日志),为什么要查看。

因此,CollectorConsumer是两个lambda接口,根据定义是有状态的。

所有其他人,例如PredicateFunctionUnaryOperatorBinaryOperatorComparator无国籍。

答案 1 :(得分:1)

  

在大多数情况下,我很难理解这一点。在哪些情况下可以接受/希望进行有状态流操作?

假设以下场景。您有一个Stream<String>,您需要按照自然顺序列出项目,每个项目都包含订单号。因此,例如,您可以输入:BananaAppleGrape。输出应该是:

1. Apple
2. Banana
3. Grape

如何在Java Stream API中解决此任务?很容易:

List<String> f = asList("Banana", "Apple", "Grape");

AtomicInteger number = new AtomicInteger(0);
String result = f.stream()
  .sorted()
  .sequential()
  .map(i -> String.format("%d. %s", number.incrementAndGet(), i))
  .collect(Collectors.joining("\n"));

现在,如果您查看此管道,您将看到3个有状态操作:

  • sorted() - 根据定义有状态。请参阅Stream.sorted()的文档:
      

    这是一项有状态的中间操作

  • map() - 本身可能是无国籍的,但在这种情况下却不是。要标记位置,您需要跟踪已标记的项目数量;
  • collect() - 是可变减少操作(从文档到Stream.collect())。根据定义,可变操作是有状态的,因为它们改变(变异)共享状态。

关于为什么sorted()是有状态的,存在一些争议。来自Stream API文档:

  

无状态操作(例如过滤器和映射)在处理新元素时不保留先前看到的元素的状态 - 每个元素都可以独立于其他元素上的操作进行处理。有状态操作(例如distinct和sorted)可以在处理新元素时包含先前看到的元素的状态。

因此,当将术语有状态 / 无状态应用于Stream API时,我们更多地谈论流的函数处理元素,而不是关于函数处理流作为一个整体

另请注意,术语无状态确定性之间存在一些混淆。它们不一样。

确定性函数在给定相同参数的情况下提供相同的结果。

无状态功能保留以前调用的状态。

这些是不同的定义。一般情况下并不依赖于彼此。决定论是关于函数结果值,而关于函数实现的无状态。

答案 2 :(得分:0)

如有疑问,只需检查文档以了解具体操作。例子:

  1. Stream.map映射器参数:

      

    mapper - 应用于每个元素的非干扰无状态函数

    这里的文档明确说明该函数必须是无状态的。

  2. Stream.forEach操作参数:

      

    action - 对元素执行的非干扰操作

    此处未指明该操作是无状态的,因此它可以是有状态的。

  3. 一般来说,它总是明确写在每个方法文档上。

答案 3 :(得分:0)

无状态函数为相同的输入返回相同的输出,&#34;无论什么&#34;。

在像Java这样的命令式语言中创建非无状态函数很容易。例如

    func = input -> currentTime();

如果我们使用有状态stream.map(func)执行func,则生成的流将取决于在运行时调用func的方式;应用程序的行为很难理解(但不是那么难)。

如果func是无状态的,stream.map(func)将始终生成相同的流,无论map如何实施和执行。这很好,也很可取。

注意&#34;无论什么&#34;意味着无状态函数必须是线程安全的。

如果函数返回void,它是否始终无状态?那么...... stateless的另一个含义 - 调用无状态函数不应该有副作用,这是重要的&#34;到申请。

如果func没有&#34;重要&#34;副作用,任意调用func是安全的。例如,即使在同一元素上,stream.map(func)也可以安全地多次调用func。 (但不要担心,Stream永远不会这样做。)

什么是重要的&#34;副作用?这是非常主观的。

至少,调用fun将花费一些CPU时间,这不是完全免费的。这可能与性能关键应用有关;或者在昂贵的平台上(咳嗽AWS)。

如果func在硬盘上记录某些东西,它可能会也可能不会是重要的&#34;副作用。 (它太贵了)

如果func查询费用过高的外部服务,那么它非常令人担忧,可能会让您破产。

现在,忘了钱。纯粹从应用程序逻辑的角度来看,func可能导致应用程序所依赖的某些状态的突变;即使func为相同的输入返回相同的输出,它仍然不能被视为&#34;无状态&#34;。例如,如果在stream.map(func)中,func将每个元素添加到列表中,稍后应用程序使用该列表,则结果列表将取决于在运行时调用func的方式。这是功能程序员所做的。

如果我们stream.forEach( e->log(e) ),它是无国籍的吗?如果

,我们可以认为它是无国籍的
  • 我们不关心log
  • 的费用
  • log()可以同时调用
  • 我们不关心日志条目的顺序
  • 日志条目对此应用程序的逻辑没有影响