【问题标题】:Stream intermediate operations ordering流中间操作排序
【发布时间】:2014-02-26 16:00:22
【问题描述】:

是否可以保证,在处理流时,中间操作将按程序顺序执行?我怀疑是这样,否则会导致非常微妙的错误,但我找不到明确的答案。

例子:

List<String> list = Arrays.asList("a", "b", "c");
List<String> modified = list.parallelStream()
        .map(s -> s + "-" + s)                 //"a-a", "b-b", "c-c"
        .filter(s -> !s.equals("b-b"))         //"a-a", "c-c"
        .map(s -> s.substring(2))              //"a", "c"
        .collect(toList());

这是否保证总是返回["a", "c"]["c", "a"]? (如果最后一个 map 操作在第一个 map 操作之前执行,那可能会抛出异常 - 类似地,如果 filter 在第二个 map 操作之后执行,“b”将保留在最终列表中)

【问题讨论】:

  • 是的,这是有保证的。
  • @LouisWasserman 如果有官方消息,你能引用吗?我找不到。
  • @assylias From this Oracle doc 我的印象是,中间操作虽然是惰性的,但只会对前一个中间操作提供的输入进行操作。我想,如果这是真的,那么它肯定会返回这些输出中的任何一个。
  • @RohitJain Stream 被实现为管道这一事实不能保证这一点吗?
  • @SotiriosDelimanolis 嗯。是的,这个概念让我认为,对特定数据的操作不会在所有中间操作之前完成,然后再处理该数据。但我还是很困惑。

标签: java java-8 java-stream


【解决方案1】:

实际上在原始问题中嵌入了几个关于排序的问题。

Holger's answer 涵盖了管道中流操作的顺序。 对于特定的流元素,管道操作必须按照程序中的说明执行,因为一般情况下,类型必须匹配,而且,因为这样做没有任何意义另一种方式。从原始示例开始,流库无法重新排序操作,就像它们已被编写一样,

List<String> modified = list.parallelStream()
    .filter(s -> !s.equals("b-b")) // these two operations are swapped
    .map(s -> s + "-" + s)         // compared to the original example
    .map(s -> s.substring(2))
    .collect(toList());

因为结果会是 [a, b, c]。这不会发生。

最初的问题询问答案是否可以是 [c, a] 而不是 [a, c]。这实际上是一个关于另一种排序的问题,我们称之为encounter order。 java.util.stream 包文档中提到了这个概念。不幸的是,我所知道的任何地方都没有明确定义它。简而言之,它与流中元素的相对定位(与执行顺序相反)以及这种定位是否具有任何语义有关。

例如,考虑来自 HashSet 和 ArrayList 的流。基于 HashSet 的流没有定义的遇到顺序,或者换句话说,它是无序的。如果您将一堆元素放入 HashSet 中,然后将它们迭代出来,它们将以某种可能与您放入它们的顺序无关的顺序出现。

但是,基于列表的流确实具有定义的遇到顺序。在原始示例中,列表是 [a, b, c],显然“a”在“b”之前,“b”在“c”之前。这种定位通常由从源到输出的流操作保留。

让我修改原来的例子来说明遭遇顺序的意义。我所做的只是更改原始列表中字符串的顺序:

List<String> list = Arrays.asList("c", "b", "a");
List<String> modified = list.parallelStream()
    .map(s -> s + "-" + s)                 //"c-c", "b-b", "a-a"
    .filter(s -> !s.equals("b-b"))         //"c-c", "a-a"
    .map(s -> s.substring(2))              //"c", "a"
    .collect(toList());

正如我们所料,输出是 [c, a]。现在让我们在集合而不是列表上运行流:

List<String> list = Arrays.asList("c", "b", "a");
Set<String> set = new HashSet<>(list);
List<String> modified = set.parallelStream()
    .map(s -> s + "-" + s)
    .filter(s -> !s.equals("b-b"))
    .map(s -> s.substring(2))
    .collect(toList());

这一次,结果是 [a, c]。 管道操作(map、filter、map)没有改变顺序,但是由于集合中元素的遇到顺序是未定义的,结果最终到达目的地以某种与之前的结果不同的顺序列出。

(我不得不更改原始列表中值的顺序,因为碰巧HashSet的迭代顺序与元素的hashcode有关,而这里给出的简单字符串示例具有连续的hashcode。)

人们可能会考虑另一种“排序”,即管道操作在不同元素之间的相对执行顺序。对于并行流,这是完全不确定的。观察这一点的一种方法是从管道操作中改变对象。 (要安全地做到这一点,被变异的对象当然必须是线程安全的,依赖任何此类副作用的顺序是不明智的。)这里有一个例子:

List<Integer> list1 = Collections.synchronizedList(new ArrayList<>());
List<Integer> list2 =
    IntStream.range(0, 10)
        .parallel()
        .boxed()
        .peek(i -> list1.add(i))
        .collect(toList());
System.out.println(list1);
System.out.println(list2);

在我的系统上,输出是:

[5, 6, 2, 3, 4, 8, 9, 7, 0, 1]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

源的相遇顺序保留到list2中的输出,但list1的顺序一般不同。事实上,list1 中元素的顺序因运行而异,而 list2 中的元素顺序始终相同。

总的来说,这里显示了三种不同的排序方式:

  • 管道操作对某些特定元素的排序;
  • 流的相遇顺序,以及
  • 管道操作在不同元素上的执行顺序。

它们都是不同的。

【讨论】:

    【解决方案2】:

    出现您的问题是因为您正在从一种类型映射到同一种类型。如果您考虑您正在执行的正式操作,很明显无法更改指定操作的顺序:

    • 您将Stream&lt;A&gt; 的项目映射到任意类型B 创建Stream&lt;B&gt;
    • 您将Filter&lt;B&gt; 应用于第一个映射的结果
    • 您将过滤后的Stream&lt;B&gt; 映射到任意类型C 创建Stream&lt;C&gt;
    • 您将C 类型的项目收集到List&lt;C&gt;

    查看这些正式的步骤应该清楚,由于类型兼容性要求,无法更改这些步骤的顺序。

    你的特殊情况中,所有三种类型都恰好是String这一事实并没有改变Streams 工作的逻辑。请记住,您用于类型参数的实际类型已被删除,并且在运行时不存在。

    Stream 实现可能会强制执行有用的操作,例如一次性执行sorteddistinct,但这需要对相同的项目和Comparator 请求这两个操作。或者简单地说,内部优化不能改变请求操作的语义。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-08-24
      • 1970-01-01
      • 2016-05-06
      • 1970-01-01
      • 1970-01-01
      • 2017-06-07
      • 1970-01-01
      • 2020-10-07
      相关资源
      最近更新 更多