我已经阅读过thisthis问题,但是仍然怀疑JDK作者是否打算观察到的Stream.skip行为。

让我们简单地输入数字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()也会影响结果。这似乎完全误导了我:我希望使用无序收集器与在终端操作之前将流转换为无序模式以及使用等效的有序收集器相同。

所以我的问题是:
  • 此行为是故意的还是错误?
  • 如果是,则在某处进行了记录?我已阅读Stream.skip()文档:它没有说明无序的终端操作。另外,Characteristics.UNORDERED文档不是很全面,也没有说整个流都将失去顺序。最后,程序包摘要中的Ordering部分也不涉及这种情况。可能我缺少什么?
  • 如果打算通过无序终端操作使整个流变为无序,为什么unordered()步骤仅从这一点开始使其无序?我可以依靠这种行为吗?还是我很幸运,我的第一个测试效果很好?
  • 最佳答案

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

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

  • 流水线的每个阶段都有一组流标志。中间操作可以注入(inject),保留或清除流标志。例如,过滤保留排序度/独特度,但不保留大小度;映射保留大小,但不保留排序或唯一性。排序注入(inject)排序。对于中间操作,标志的处理非常简单,因为所有决策都是本地的。

    对于终端操作,标志的处理更加微妙。 ORDERED是与终端操作最相关的标志。如果终端op是UNDERDERED,则我们反向传播无序度。

    我们为什么要做这个?好吧,请考虑以下管道:
    set.stream()
       .sorted()
       .forEach(System.out::println);
    

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



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



    它应该只是实现细节;如果正确实现,您将不会注意到(但您的流更快)。

    09-25 22:04