【问题标题】:Stream.skip behavior with unordered terminal operationStream.skip 行为与无序终端操作
【发布时间】:2015-06-15 10:50:03
【问题描述】:

我已经阅读了 this 和 this 的问题,但仍然怀疑观察到的 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

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

所以我的问题是:

  1. 这种行为是有意的还是一个错误?
  2. 如果是,是否记录在某处?我已阅读 Stream.skip() 文档:它没有说明无序终端操作。 Characteristics.UNORDERED 文档也不是很容易理解,也没有说整个流的排序都会丢失。最后,包摘要中的Ordering 部分也不涵盖这种情况。可能我错过了什么?
  3. 如果打算无序的终端操作使整个流无序,为什么unordered() 步骤仅从这一点开始使其无序?我可以依靠这种行为吗?还是我很幸运,我的第一个测试运行良好?

【问题讨论】:

  • 但这就是问题 - 没有“之前”。前面的所有操作都是中间操作,只有在你执行了终端操作时才会发生流缩减 - 在这种情况下为收集。
  • 正如我已经说过的there,如果行为一致,则更容易理解。这仍然允许故意在该问题中显示的行为,但是我们可能会将订单保留得太频繁的事实视为错误。你知道,sorted().forEach() 应该不排序。
  • 您的初始代码行是否缺少boxed() 调用?没有boxed(),我不能像那样collect()。
  • @Thomas,谢谢,boxed() 已添加。 .parallelStream().filter(x -&gt; x &gt; 0) 是必要的,因为我想揭示问题,而不是消除它们:-) 当然这是一个人为的简化示例。实际上,如果您使用例如bufferedReader.lines().skip(1).parallel().forEach(...),则可能会出现此类问题。请参阅链接的问题。
  • @FedericoPeraltaSchaffner,如果你解析一些带有标题行的文本文件并进行高 Q 处理,那么lines.stream().skip(1).parallel().blahblah 可能对你很有效。

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


【解决方案1】:

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

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

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

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

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

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

由于forEach不限于按顺序操作,所以排序列表的工作完全是白费力气。所以我们反向传播这个信息(直到我们碰到一个短路操作,比如limit),以免失去这个优化机会。同样,我们可以在无序流上使用distinct 的优化实现。

这种行为是有意的还是一个错误?

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

如果是,它是否记录在某处?

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

【讨论】:

  • 谢谢!这正是我一直在等待的答案 :-) 我已经在我的库中实现了一个 skipOrdered 方法来解决这个错误。它采用流拆分器,将其转换为顺序,执行skip(),然后在必要时将其转回parallel()。希望原来的skip()会在JDK9中被修复,所以这个方法就没有必要了。
  • 经过一些分析,我们选择完全退出反向传播。唯一能得到回报的地方是优化排序;如果您有一个排序为无序终端操作的管道,那么无论如何这可能是用户错误。
  • @Brian Goetz:为了正确起见,终端操作不会再反向传播无序属性?那么这是否意味着forEach和forEachOrdered在这方面没有区别?
  • @Holger 正确,不再有终端标志的反向传播,这意味着终端操作的有序性(或缺乏)不会影响 earlier的行为> 操作。当然forEach和forEachOrdered.还是有区别的
【解决方案2】:

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

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

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

这是一个 {@link Collector.Characteristics#UNORDERED 无序} 收藏家。

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

表示采集操作不提交保存 输入元素的相遇顺序

在 Collector 的 Javadocs 中我们也可以看到:

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

这对我来说意味着,如果我们设置 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 特征的),对我来说意味着您确实关心排序,因此,元素被按顺序提取并且您得到预期的行为。

【讨论】:

  • 感谢您对我的问题的关注,但这根本不能回答问题。 “对于并发收集器”部分无关紧要,因为所讨论的收集器都没有 CONCURRENT 特征。我知道toSet 是无序的,所以它将终端操作变为无序模式,我在问题中提到了这一点。我还提到删除 unordered() 不会改变任何事情,所以当我省略 unordered() 时,我知道相同的行为。我不是在谈论性能,只是关于正确性,因此skip() 是否便宜不在问题范围内。
  • 最后一句话是关于“无序流源”或unordered()中间操作。这些工作得很好。它没有说明我遇到问题的无序终端操作。当然我知道Collectors.toCollection(HashSet::new) 是有序收集器。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-08-13
  • 2018-11-19
  • 2020-01-22
  • 1970-01-01
  • 2019-08-12
  • 2011-03-03
  • 2019-11-12
相关资源
最近更新 更多