使用无序终端操作的Stream.skip行为

时间:2015-06-15 10:50:03

标签: java parallel-processing java-8 java-stream collectors

我已经阅读了thisthis个问题,但仍然怀疑Stream.skip的观察行为是否是JDK作者想要的。

让我们简单输入数字1..20:

List<Integer> input = IntStream.rangeClosed(1, 20).boxed().collect(Collectors.toList());

现在让我们创建一个并行流,以不同的方式将unordered()skip()结合起来并收集结果:

System.out.println("skip-skip-unordered-toList: "
        + input.parallelStream().filter(x -> x > 0)
            .skip(1)
            .skip(1)
            .unordered()
            .collect(Collectors.toList()));
System.out.println("skip-unordered-skip-toList: "
        + input.parallelStream().filter(x -> x > 0)
            .skip(1)
            .unordered()
            .skip(1)
            .collect(Collectors.toList()));
System.out.println("unordered-skip-skip-toList: "
        + input.parallelStream().filter(x -> x > 0)
            .unordered()
            .skip(1)
            .skip(1)
            .collect(Collectors.toList()));

过滤步骤在这里基本上没有任何作用,但为流引擎增加了更多的难度:现在它不知道输出的确切大小,因此关闭了一些优化。我有以下结果:

skip-skip-unordered-toList: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
// absent values: 1, 2
skip-unordered-skip-toList: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20]
// absent values: 1, 15
unordered-skip-skip-toList: [1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 20]
// absent values: 7, 18

结果完全没问题,一切都按预期工作。在第一种情况下,我要求跳过前两个元素,然后收集列表,没有特别的顺序。在第二种情况下,我要求跳过第一个元素,然后转为无序并跳过一个元素(我不关心哪一个)。在第三种情况下,我首先转为无序模式,然后跳过两个任意元素。

让我们跳过一个元素并以无序模式收集到自定义集合。我们的自定义收藏将是HashSet

System.out.println("skip-toCollection: "
        + input.parallelStream().filter(x -> x > 0)
        .skip(1)
        .unordered()
        .collect(Collectors.toCollection(HashSet::new)));

输出结果令人满意:

skip-toCollection: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
// 1 is skipped

所以一般来说,我希望只要流被排序,skip()就会跳过第一个元素,否则会跳过任意​​元素。

但是,让我们使用等效的无序终端操作collect(Collectors.toSet())

System.out.println("skip-toSet: "
        + input.parallelStream().filter(x -> x > 0)
            .skip(1)
            .unordered()
            .collect(Collectors.toSet()));

现在输出是:

skip-toSet: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 17, 18, 19, 20]
// 13 is skipped

使用任何其他无序终端操作(如forEachfindAnyanyMatch等)可以实现相同的结果。在这种情况下删除unordered()步骤不会改变任何内容。似乎虽然unordered()步骤正确地使流从当前操作开始无序,但无序终端操作使整个流从一开始就开始无序,尽管如果使用skip()这会影响结果。这对我来说似乎完全是误导:我希望使用无序收集器与在终端操作之前将流转换为无序模式并使用等效的有序收集器相同。

所以我的问题是:

  1. 这种行为是打算还是错误?
  2. 如果是,它在某处记录了吗?我已经阅读了Stream.skip()文档:它没有说明无序终端操作。另外Characteristics.UNORDERED文档也不是很清楚,并没有说整个流的排序会丢失。最后,包摘要中的Ordering部分也未涵盖此案例。可能我错过了什么?
  3. 如果无意的终端操作意图使整个流无序,那么为什么unordered()步骤仅在此之后才使其无序?我可以依靠这种行为吗?或者我很幸运,我的第一次测试工作得很好?

2 个答案:

答案 0 :(得分:21)

回想一下,流标志(ORDERED,SORTED,SIZED,DISTINCT)的目标是使操作能够避免做不必要的工作。涉及流标志的优化示例如下:

  • 如果我们知道流已经排序,那么sorted()就是无操作;
  • 如果我们知道流的大小,我们可以在toArray()中预先分配一个正确大小的数组,避免复制;
  • 如果我们知道输入没有有意义的遭遇顺序,我们无需采取额外步骤来保留遭遇顺序。

管道的每个阶段都有一组流标志。中间操作可以注入,保留或清除流标志。例如,过滤保留了sorted-ness / distinct-ness但不保留大小;映射保留大小但不是排序或不同的。排序注入排序。中间操作的标志处理相当简单,因为所有决策都是本地的。

终端操作的标志处理更加微妙。 ORDERED是终端操作最相关的标志。如果终端操作是UNORDERED,那么我们会反向传播无序的。

为什么我们这样做?好吧,考虑一下这个管道:

set.stream()
   .sorted()
   .forEach(System.out::println);

由于forEach不受约束按顺序操作,因此对列表进行排序的工作完全是浪费精力。因此,我们反向传播此信息(直到我们遇到短路操作,例如limit),以免失去此优化机会。同样,我们可以在无序流上使用distinct的优化实现。

  

这种行为是打算还是错误?

是:)反向传播是预期的,因为它是一种有用的优化,不应产生不正确的结果。但是,bug部分是我们正在传播过去的skip,我们不应该这样做。所以UNORDERED标志的反向传播是过于激进的,这是一个错误。我们会发布一个错误。

  

如果是,它在某处记录了吗?

它应该只是一个实现细节;如果它被正确实现,你就不会注意到(除了你的流更快。)

答案 1 :(得分:0)

  

@Ruben,你可能不明白我的问题。大致问题   是:为什么unordered()。collect(toCollection(HashSet :: new))表现   与collect(toSet())不同。当然我知道toSet()是   无序的。

可能,但是,无论如何,我会再试一次。

看一下收集器的Javadocs toSet和 toCollection ,我们可以看到 toSet 提供了一个无序的收集器

  

这是{@link Collector.Characteristics#UNORDERED unordered}   收集器。

即具有 UNORDERED 特征的 CollectorImpl 。看一下Collector.Characteristics#UNORDERED的Javadoc,我们可以读到:

  

表示收集操作未提交保留   输入元素的遭遇顺序

在收藏家的Javadocs中我们也可以看到:

  

对于并发收集器,实现是免费的(但不是   要求同时实施减少。同时减少   是一个从中同时调用累加器函数的函数   多个线程,使用相同的可同时修改的结果   容器,而不是在结果期间保持结果   积累。只有在应用时才应用并发减少   collector具有{@link Characteristics#UNORDERED}特征或   如果原始数据是无序的

这对我来说,如果我们设置 UNORDERED 特性,我们根本不关心流的元素传递给累加器的顺序,因此,可以按任何顺序从管道中提取元素。

顺便说一句,如果省略示例中的无序(),则会得到相同的行为:

    System.out.println("skip-toSet: "
            + input.parallelStream().filter(x -> x > 0)
                .skip(1)
                .collect(Collectors.toSet()));

此外,Stream中的skip()方法给出了一个提示:

  

虽然{@code skip()}通常是顺序上的廉价操作   流管道,在有序并行上可能相当昂贵   管道

  

使用无序流源(例如{@link #generate(Supplier)})   或者使用{@link #unordered()}删除排序约束   导致显着的加速

使用时

Collectors.toCollection(HashSet::new)

你正在创建一个普通的“有序”收集器(一个没有UNORDERED特征的收集器),对我来说意味着你关心排序,因此,元素按顺序被提取并且你得到预期的行为。