Stream.reduce 始终保持并行无序流的顺序

Stream.reduce always preserving order on parallel, unordered stream

前面的几个问题都做了, answer by Brian Goetz, as well as the javadoc for Stream.reduce(), and the java.util.stream package javadoc,但是还是没搞懂下面的问题:

拿这段代码来说:

  public static void main(String... args) {
    final String[] alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
    System.out.println("Alphabet: ".concat(Arrays.toString(alphabet)));
    System.out.println(new HashSet<>(Arrays.asList(alphabet))
          .parallelStream()
          .unordered()
          .peek(System.out::println)
          .reduce("", (a,b) -> a + b, (a,b) -> a + b));
  }

为什么减少总是*保留相遇顺序?

为了帮助解释这一点,我将把这个字符串的范围缩小到 ABCD

并行流会将字符串分成两部分:ABCD。当我们稍后组合这些时,AB 端的结果将是传递给函数的第一个参数,而 CD 端的结果将是传递给函数的第二个参数。这与两者中的哪一个实际先完成无关。

unordered 运算符会影响流上的一些操作,例如 limit 操作,它不会影响简单的 reduce.

首先 unordered 并不意味着 实际的 洗牌;它所做的一切都为 Stream 管道设置了一个标志 - 以后可以利用它。

源元素的洗牌可能比流管道本身的操作更昂贵,因此实现可能选择不这样做(就像在这种情况下)。

jdk-8jdk-9 目前 (测试并查看了来源)- reduce 没有考虑到这一点.请注意,这很可能会在未来的构建或版本中发生变化。

此外,当您说 unordered - 您实际上是说您 不关心该顺序 并且返回相同结果的流并不违反该顺序规则。

例如通知 this question/answer 解释 findFirst 例如(只是另一个终端操作)更改为在 java 中考虑 unordered -9 而不是 java-8。

TLDR:.reduce() 并不总是保持顺序,其结果基于流拆分器的特性。

分离器

stream的encounter order依赖于stream spliterator(前面回答的None)。

根据源流有不同的拆分器。您可以从这些集合的源代码中获取拆分器的类型。

HashSet -> HashMap#KeySpliterator = 未排序

ArrayDeque = 有序

ArrayList = 有序

TreeSet -> TreeMap#Spliterator = 有序和排序

logicbig.com - Ordering logicbig.com - Stateful vs Stateless

此外,您可以应用 .unordered() 中间流操作,指定流中的后续操作不应依赖于顺序。

受 spliterator 和 .unordered() 方法使用影响的流操作(主要是有状态的)是:

  • .findFirst()
  • .limit()
  • .skip()
  • .distinct()

这些操作将根据流及其拆分器的顺序 属性 为我们提供不同的结果。

.peek() 方法不考虑顺序,如果并行执行流,它总是 print/receive 个元素无序。

.reduce()

现在是终端 .reduce() 方法。中间操作 .unordered() 对拆分器的类型没有任何影响(如@Eugene 所述)。但重要的是,它仍然与源拆分器中的一样。如果源拆分器是有序的,则 .reduce() 的结果将是有序的,如果源是无序的,则 .reduce() 的结果将是无序的。

您正在使用新的 HashSet<>(Arrays.asList(alphabet)) 来获取流的实例。它的拆分器是无序。您将结果排序只是巧合,因为您使用单个字母表字符串作为流的元素,而无序结果实际上是相同的。现在,如果您将其与数字混合或将其与小写和大写混合,那么这不再适用。例如采用以下输入,第一个是您发布的示例的子集:

HashSet .reduce() - 无序

"A","B","C","D","E","F" -> "ABCDEF"
"a","b","c","1","2","3","A","B","C" -> "a1Ab2Bc3C"
"Apple","Orange","Banana","Mango" -> "AppleMangoOrangeBanana"

TreeSet .reduce() - 已排序,已排序

"A","B","C","D","E","F" -> "ABCDEF"
"a","b","c","1","2","3","A","B","C" -> "123ABCabc"
"Apple","Orange","Banana","Mango" -> "AppleBananaMangoOrange"

ArrayList .reduce() - 有序

"A","B","C","D","E","F" -> "ABCDEF"
"a","b","c","1","2","3","A","B","C" -> "abc123ABC"
"Apple","Orange","Banana","Mango" -> "AppleOrangeBananaMango"

您看到仅使用字母表源流测试 .reduce() 操作可能会导致错误的结论。

答案是.reduce()并不总是保持顺序,它的结果是基于stream spliterator的特性。