Java流操作执行顺序在终端点处的问题

6

我一直在尝试从Java官方文档中找到清晰的协议,确定调用终止操作后Java流会按照什么顺序处理元素并调用中间操作。

例如,让我们看一下这些示例,它们使用Java流版本和普通迭代版本(都产生相同的结果)

示例1:

    List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
    Function<Integer, Integer> map1 = i -> i;
    Predicate<Integer> f1 = i -> i > 2;

    public int findFirstUsingStreams(List<Integer> ints){
        return ints.stream().map(map1).filter(f1).findFirst().orElse(-1);
    }

    public int findFirstUsingLoopV1(List<Integer> ints){
        for (int i : ints){
            int mappedI = map1.apply(i);
            if ( f1.test(mappedI) ) return mappedI;
        }
        return -1;
    }

    public int findFirstUsingLoopV2(List<Integer> ints){
        List<Integer> mappedInts = new ArrayList<>( ints.size() );

        for (int i : ints){
            int mappedI = map1.apply(i);
            mappedInts.add(mappedI);
        }

        for (int mappedI : mappedInts){
            if ( f1.test(mappedI) ) return mappedI;
        }
        return -1;
    }
< p > Java 中 findFirstUsingStreams 方法中的流是否会按照 findFirstUsingLoopV1 中描述的顺序运行 map1map 不会为所有元素运行),还是按照 findFirstUsingLoopV2 中描述的顺序运行(map 会为所有元素运行)?

并且在将来的 Java 版本中,这个顺序会改变吗?或者有官方文档保证 map1 调用的顺序吗?

Example2:

Predicate<Integer> f1 = i -> i > 2;
Predicate<Integer> f2 = i -> i > 3;


public List<Integer> collectUsingStreams(List<Integer> ints){
    return ints.stream().filter(f1).filter(f2).collect( Collectors.toList() );
}

public List<Integer> collectUsingLoopV1(List<Integer> ints){
    List<Integer> result = new ArrayList<>();
    for (int i : ints){
        if ( f1.test(i) && f2.test(i) ) result.add(i);
    }
    return result;
}

public List<Integer> collectUsingLoopV2(List<Integer> ints){
    List<Integer> result = new ArrayList<>();
    for (int i : ints){
        if ( f2.test(i) && f1.test(i) ) result.add(i);
    }
    return result;
}

在调用collect方法后,collectUsingStreams方法中的Java流程是否会按照collectUsingLoopV1中描述的顺序运行f1f2>(f1f2之前被评估),还是按照collectUsingLoopV2中描述的顺序(f2f1之前被评估)?

未来版本的Java中,这个顺序会改变吗?或者有官方文档保证f1f2调用的顺序吗?

编辑

感谢所有的答案和评论,但很遗憾我仍然没有看到关于处理元素顺序的好解释。文档确实说列表的遇到顺序将被保留,但它们并没有指定这些元素将如何被处理。例如,在findFirst的情况下,文档保证map1将首先看到1,然后是2,但它并没有说map1不会对4和5执行。这是否意味着我们不能保证我们期望的处理顺序将在未来的Java版本中得到满足?可能是的。


2
没有任何顺序的保证,只要结果不改变,顺序可以是任何东西。函数式编程假定没有副作用,因此如果您有一个依赖于处理顺序的副作用,这将打破这个基本假设。 - Peter Lawrey
2
你在询问两个顺序:处理顺序(它未定义为保留 - 与您的流并行并查看)和遇到顺序,如果流的源是有序的且没有中间操作打破它,无论是并行还是顺序,都被定义为保留。 - Eugene
1
我认为Example1是故意未定义的,在实践中取决于管道中是否有有状态操作。对于Example2,我认为顺序必须遵循管道顺序。我不确定它是否被记录在哪里,但我认为流的使用的核心前提是操作按顺序执行。 - shmosel
1
@shmosel确实,第二个例子之所以有效,是因为两个过滤器是独立的,这是实现无法假设的。有多少个流管道以filter(Objects::nonNull)开头?如果允许推迟(与后续操作交换)该检查,它将破坏整个操作。 - Holger
显示剩余2条评论
1个回答

12
“那么这个顺序会在Java的未来版本中改变吗?还是有官方文件保证了map1调用的顺序?”
Java文档,包括包摘要(有时人们会忽略这些),都是API合同。如果Javadocs没有定义但可以观察到的行为,通常应将其视为实现细节,可能会在未来版本中更改。所以如果在Javadocs中找不到,则不能保证。
流管道阶段的调用和交错的顺序未指定。指定的是在哪些情况下保留所谓的相遇顺序。假设有一个有序的流,实现仍然允许执行任何交错、分批和内部重新排序,以保持相遇顺序。例如,sorted(comparator).filter(predicate).findFirst() 可以在内部替换为 filter(predicate).min(comparator),这显然会显著影响Predicate和Comparator的调用方式,但即使在有序流中也会产生相同的结果。
“这是否意味着我们无法保证我们处理的顺序将如我们所期望的那样在Java的未来版本中?可能是的。”
是的,这不应该成为问题,因为大多数流API 需要回调函数是无状态没有副作用, 这意味着它们不应该关心流管道的内部执行顺序,结果应该是相同的,除了无序流允许的余地。显式要求和缺乏保证给JDK开发人员提供了实现流的灵活性。
如果您有任何特定情况需要考虑,请提出更具体的问题,询问您想要避免的执行重排序。
你应该时刻记住,流可以是并行的,例如由第三方代码传递的实例,或包含一个源或中间流操作,其惰性程度比理论上可能的要低(目前flatMap就是这样的操作)。如果有人提取和重新包装spliterator或使用Stream interface的自定义实现,则流管道也可以包含自定义行为。因此,尽管特定的流实现在特定方式下可能会表现出一些可预测的行为,并且未来针对该特定情况的优化可能被认为是高度不可能的,但这并不适用于所有可能的流管道,因此API无法提供这样的一般性保证。

2
那是一个有趣的例子,使用了排序!再加一。 - Eugene
@Holger的意思是f1f2可以互换吗? - Eugene
@Eugene 考虑使用 .filter(Objects::nonNull) .filter(e -> e.someProperty() == x) - Holger
@Holger 好的,我明白了。但在这种情况下,我认为没有理由不能替换f1f2(即使这可能会变得绝对),只是我不知道jvm首先应该如何推理,认为这是不可能的。 - Eugene
@Eugene 我不知道你所说的“为什么f1f2不能被替换”。无论JRE做什么,它都必须保持语义,即在我的示例中不抛出NPE。在维护语义时,它可以做任何想做的事情。当存在filter(x -> false)时,可以快速缩短整个操作,当只有filter(x -> true)时,可以保持大小等。虽然当前的JVM不支持,但是在当前的反射能力之上实现它甚至是可能的,但会导致不必要的开销,因为x -> truex -> false在现实生活中很少发生... - Holger
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接