【问题标题】:Intermediate stream operations not evaluated on count未按计数评估中间流操作
【发布时间】:2020-01-04 15:21:57
【问题描述】:

我似乎无法理解 Java 如何将流操作组合到流管道中。

执行以下代码时

public
 static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

控制台只打印4StringBuilder 对象的值仍然是 ""

当我添加过滤操作时:filter(s -> true)

public static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .filter(s -> true)
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

输出变为:

4
1234

这个看似多余的过滤器操作如何改变组合流管道的行为?

【问题讨论】:

  • 有趣!!!
  • 我想这是特定于实现的行为;可能是因为第一个流的大小已知,而第二个流没有,大小决定了是否执行中间操作。
  • 出于兴趣,如果你反转过滤器和映射会发生什么?
  • 在 Haskell 中进行了一些编程,闻起来有点像这里正在进行的一些懒惰的评估。谷歌搜索返回,流确实有些懒惰。可能是这样吗?并且没有过滤器,如果java足够聪明,就不需要实际执行映射。
  • @AndyTurner 它给出了相同的结果,即使是反转

标签: java java-stream


【解决方案1】:

count() 终端操作,在我的 JDK 版本中,最终会执行以下代码:

if (StreamOpFlag.SIZED.isKnown(helper.getStreamAndOpFlags()))
    return spliterator.getExactSizeIfKnown();
return super.evaluateSequential(helper, spliterator);

如果在操作的管道中有filter() 操作,那么最初已知的流的大小将无法再知道(因为filter 可能会拒绝流的某些元素)。所以if块没有被执行,中间操作被执行,StringBuilder因此被修改。

另一方面,如果管道中只有map(),则流中的元素数量保证与初始元素数量相同。所以执行了if块,直接返回size,不求中间操作。

请注意,传递给map() 的 lambda 违反了文档中定义的合同:它应该是一个不干涉的、无状态的操作,但它不是无状态的。所以在这两种情况下产生不同的结果不能被认为是一个错误。

【讨论】:

  • 因为flatMap() 可能能够改变元素的数量,这就是它最初渴望(现在懒惰)的原因吗?因此,如果map() 以其当前形式违反合同,我猜,另一种方法是使用forEach() 并单独计算。
  • 关于 flatMap,我不这么认为。是的,AFAIK,因为它最初更容易让它变得渴望。是的,使用带有 map() 的流来产生副作用是个坏主意。
  • 您对如何在不使用额外过滤器或在 map() 操作中产生副作用的情况下实现完整输出 4 1234 有什么建议吗?
  • int count = array.length; String result = String.join("", array);
  • 或者如果你真的想使用StringBuilder,你可以使用forEach,或者你可以使用Collectors.joining("")
【解决方案2】:

jdk-9 中,Java 文档中清楚地记录了它

消除副作用也可能令人惊讶。除了终端操作 forEach 和 forEachOrdered 之外,当流实现可以优化行为参数的执行而不影响计算结果时,可能并不总是执行行为参数的副作用。 (有关具体示例,请参阅count 操作中记录的 API 说明。)

API 说明:

如果实现可以直接从流源计算计数,则可以选择不执行流管道(顺序或并行)。在这种情况下,不会遍历任何源元素,也不会评估任何中间操作。 具有副作用的行为参数可能会受到影响,除了调试等无害的情况外,强烈建议不要使用这些参数。例如,考虑以下流:

 List<String> l = Arrays.asList("A", "B", "C", "D");
 long count = l.stream().peek(System.out::println).count();

流源 List 所覆盖的元素数量是已知的,并且中间操作 peek 不会向流中注入或删除元素(可能是 flatMap 或过滤器操作的情况) . 因此,计数是列表的大小,不需要执行管道,并且作为副作用,打印出列表元素。

【讨论】:

    【解决方案3】:

    这不是 .map 的用途。它应该被用来将“Something”流转换为“Something Else”流。在这种情况下,您使用 map 将字符串附加到外部 Stringbuilder,之后您有一个“Stringbuilder”流,每个流都是由 map 操作创建的,将一个数字附加到原​​始 Stringbuilder。

    您的流实际上并没有对流中的映射结果做任何事情,因此假设流处理器可以跳过该步骤是完全合理的。您指望副作用来完成这项工作,这会破坏地图的功能模型。使用 forEach 执行此操作会为您提供更好的服务。将计数完全作为一个单独的流,或者在 forEach 中使用 AtomicInt 放置一个计数器。

    过滤器强制它运行流内容,因为它现在必须对每个流元素做一些在概念上有意义的事情。

    【讨论】:

      猜你喜欢
      • 2019-08-12
      • 2011-12-08
      • 1970-01-01
      • 2019-10-22
      • 1970-01-01
      • 1970-01-01
      • 2014-02-08
      • 1970-01-01
      相关资源
      最近更新 更多