java 流是否能够从 map/filter 条件下延迟减少?

Are java streams able to lazilly reduce from map/filter conditions?

我正在使用函数式编程风格来解决 Leetcode 简单问题,Count the Number of Consistent Strings。这道题的前提很简单:统计“所有值都在另一个集合中”谓词成立的值的数量。

我有两种方法,一种我相当确定其行为符合我的要求,另一种我不太确定。两者都产生正确的输出,但理想情况下,它们会在输出处于最终状态后停止评估其他元素。


    public int countConsistentStrings(String allowed, String[] words) {
        final Set<Character> set = allowed.chars()
          .mapToObj(c -> (char)c)
          .collect(Collectors.toCollection(HashSet::new));
        return (int)Arrays.stream(words)
          .filter(word ->
                  word.chars()
                  .allMatch(c -> set.contains((char)c))
                 )
          .count();
    }

在此解决方案中,据我所知,allMatch 语句将在谓词不成立的第一个 c 实例处终止并评估为 false,从而跳过该流中的其他值。


    public int countConsistentStrings(String allowed, String[] words) {
        Set<Character> set = allowed.chars()
          .mapToObj(c -> (char)c)
          .collect(Collectors.toCollection(HashSet::new));
        return (int)Arrays.stream(words)
          .filter(word ->
                  word.chars()
                  .mapToObj(c -> set.contains((char)c))
                  .reduce((a,b) -> a&&b)
                  .orElse(false)
                 )
          .count();
    }

在此解决方案中,使用了相同的逻辑,但我使用了 map,然后是 reduce,而不是 allMatch。从逻辑上讲,在单个 false 值来自 map 阶段后,reduce 将始终计算为 false。我知道 Java 流是懒惰的,但我不确定它们何时“'know'”它们到底有多懒惰。这会比使用 allMatch 效率低还是懒惰会确保相同的操作?


最后,在这段代码中,我们可以看到 x 的值将始终为 0,因为仅过滤正数后,它们的总和将始终为正数(假设没有溢出),因此取正数的最小值和硬编码的 0 将为 0。流是否足够懒惰以始终将其评估为 0,或者它会在过滤器之后减少每个元素吗?

List<Integer> list = new ArrayList<>();
...
/*Some values added to list*/
...
int x = list.stream()
        .filter(i -> i >= 0)
        .reduce((a,b) -> Math.min(a+b, 0))
        .orElse(0);

综上所述,如何知道 Java 流何时会延迟?我在代码中看到了偷懒的机会,但我怎么能保证我的代码尽可能地偷懒?

您要求的实际术语是 short-circuiting

Further, some operations are deemed short-circuiting operations. An intermediate operation is short-circuiting if, when presented with infinite input, it may produce a finite stream as a result. A terminal operation is short-circuiting if, when presented with infinite input, it may terminate in finite time. Having a short-circuiting operation in the pipeline is a necessary, but not sufficient, condition for the processing of an infinite stream to terminate normally in finite time.

术语“惰性”仅适用于中间操作,意味着它们仅在终端操作请求时才执行工作。情况总是如此,因此当您不链接终端操作时,任何中间操作都不会处理任何元素。

判断终端操作是否短路,很简单。转到 the Stream API documentation 并检查特定终端操作的文档是否包含句子

This is a short-circuiting terminal operation.

allMatch has it, reduce has not.

这并不意味着这种基于逻辑或代数的优化是不可能的。但责任在于 JVM 的优化器,它可能会为循环做同样的事情。但是,这需要内联所有涉及的方法以确保此条件始终适用并且没有必须保留的副作用。这种行为兼容性意味着即使处理得到优化,peek(System.out::println) 也会继续打印所有元素,就好像它们已被处理一样。实际上,您不应该期待这样的优化,因为 Stream 实现代码对于优化器来说太复杂了。