Java 8 Stream API-任何有状态的中间操作都可以保证新的源集合吗?

时间:2018-09-11 09:11:39

标签: java java-8 java-stream

以下陈述正确吗?

  

sorted()操作是“有状态的中间操作”,这意味着后续操作不再对后备集合进行操作,而是对内部状态进行操作。

Sourcesource-它们似乎是彼此复制或来自同一来源。)

我已经从以上来源测试了Stream::sorted作为摘要:

final List<Integer> list = IntStream.range(0, 10).boxed().collect(Collectors.toList());

list.stream()
    .filter(i -> i > 5)
    .sorted()
    .forEach(list::remove);

System.out.println(list);            // Prints [0, 1, 2, 3, 4, 5]

有效。我用Stream::sortedStream::distinctStream::limit替换了Stream::skip

final List<Integer> list = IntStream.range(0, 10).boxed().collect(Collectors.toList());

list.stream()
    .filter(i -> i > 5)
    .distinct()
    .forEach(list::remove);          // Throws NullPointerException

令我惊讶的是,抛出了NullPointerException

所有经过测试的方法都遵循stateful intermediate operation特性。但是,Stream::sorted的这种独特行为未得到记录,Stream operations and pipelines部分也没有说明有状态中间操作是否真正保证了新的源集合。

我的困惑来自哪里,上述行为的解释是什么?

3 个答案:

答案 0 :(得分:31)

API文档没有做出这样的保证“后续操作将不再对后备集合进行操作”,因此,您永远不应依赖于特定实现的这种行为。

您的榜样偶然发生了想要的事情;甚至不能保证List创建的collect(Collectors.toList())支持remove操作。

显示反例

Set<Integer> set = IntStream.range(0, 10).boxed()
    .collect(Collectors.toCollection(TreeSet::new));
set.stream()
    .filter(i -> i > 5)
    .sorted()
    .forEach(set::remove);

抛出一个ConcurrentModificationException。原因是实现已优化了此方案,因为源已被排序。原则上,它可以对原始示例进行相同的优化,因为forEach以没有指定的顺序显式执行操作,因此不需要排序。

还有其他可以想象的优化,例如sorted().findFirst()可以转换为“查找最小值”操作,而无需将元素复制到新存储中进行排序。

因此,最重要的是,当依靠未指定的行为时,今天可能会发生的事情,明天可能会在添加新的优化措施时中断。

答案 1 :(得分:7)

好吧sorted必须成为流管道的完整复制障碍,毕竟您的所有源都可能被未排序;但这没有被记录下来,因此请不要依赖它。

这不是sorted本身的 ,而是可以对流管道进行其他优化,以便可以完全跳过sorted。例如:

List<Integer> sortedList = IntStream.range(0, 10)
            .boxed()
            .collect(Collectors.toList());

    StreamSupport.stream(() -> sortedList.spliterator(), Spliterator.SORTED, false)
            .sorted()
            .forEach(sortedList::remove); // fails with CME, thus no copying occurred 

当然,sorted必须成为一个完整的障碍,并停止进行完整的排序,除非可以跳过它,因此文档没有做出这样的承诺,因此我们不会运行奇怪的惊喜。

另一方面,

distinct不必完全成为障碍,唯一不同的是一次检查一个元素(如果唯一);因此,在检查了一个元素(并且它是唯一的)之后,该元素将传递到下一阶段,因此不会成为一个完整的障碍。无论哪种方式,这也都没有记录...

答案 2 :(得分:3)

您不应该使用终端操作forEach(list::remove)来提起诉讼,因为list::remove是一种干扰功能,并且违反了"non-interference"的终端操作原则。

在怀疑为什么不正确的代码片段会导致意外(或未记录的)行为之前,务必遵循规则。

我相信list::remove是这里问题的根源。如果您为forEach编写了适当的操作,则不会注意到此方案的操作之间的差异。