流中间操作排序

时间:2014-02-26 16:00:23

标签: java java-8 java-stream

是否可以保证在使用流时,中间操作将按程序顺序执行?我怀疑是这种情况,或者它会导致非常微妙的错误,但我找不到明确的答案。

示例:

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"]吗? (如果最后一个映射操作在第一个映射操作之前执行,则可能抛出异常 - 类似地,如果在第二个映射操作之后执行过滤器,则“b”将保留在最终列表中)

2 个答案:

答案 0 :(得分:10)

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

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]。这实际上是一个关于不同类型排序的问题,我们将其称为遭遇订单。 java.util.stream包文档中提到了这个概念。不幸的是,在我所知道的任何地方都没有明确定义。简而言之,它涉及流中元素的相对定位(与执行顺序相对)以及此定位是否具有任何语义。

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

但是,基于List的流确实具有已定义的遭遇顺序。在原始示例中,列表是[a,b,c],并且显然“a”在“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的迭代顺序与元素的哈希码有关,这里给出的简单字符串示例都有连续的哈希码。)< / p>

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

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中元素的顺序始终相同。

总之,这里显示了三种不同的排序:

  • 某些特定元素的管道操作顺序;
  • 流的遭遇顺序和
  • 不同元素上的管道操作的执行顺序。

他们都是截然不同的。

答案 1 :(得分:6)

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

  • 您将Stream<A>的项目映射到任意类型B,从而创建Stream<B>
  • 您对第一个映射的结果应用Filter<B>
  • 您将已过滤的Stream<B>映射到任意类型C,从而创建Stream<C>
  • 您将C类型的项目收集到List<C>

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

你的特殊情况所有三种类型碰巧都是String的事实并没有改变Stream的工作原理。请记住,用于类型参数的实际类型将被删除,并且在运行时不存在。

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