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
。
并行流会将字符串分成两部分:AB
和 CD
。当我们稍后组合这些时,AB
端的结果将是传递给函数的第一个参数,而 CD
端的结果将是传递给函数的第二个参数。这与两者中的哪一个实际先完成无关。
unordered
运算符会影响流上的一些操作,例如 limit
操作,它不会影响简单的 reduce
.
首先 unordered
并不意味着 实际的 洗牌;它所做的一切都为 Stream 管道设置了一个标志 - 以后可以利用它。
源元素的洗牌可能比流管道本身的操作更昂贵,因此实现可能选择不这样做(就像在这种情况下)。
jdk-8
和 jdk-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的特性。
前面的几个问题都做了
拿这段代码来说:
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
。
并行流会将字符串分成两部分:AB
和 CD
。当我们稍后组合这些时,AB
端的结果将是传递给函数的第一个参数,而 CD
端的结果将是传递给函数的第二个参数。这与两者中的哪一个实际先完成无关。
unordered
运算符会影响流上的一些操作,例如 limit
操作,它不会影响简单的 reduce
.
首先 unordered
并不意味着 实际的 洗牌;它所做的一切都为 Stream 管道设置了一个标志 - 以后可以利用它。
源元素的洗牌可能比流管道本身的操作更昂贵,因此实现可能选择不这样做(就像在这种情况下)。
jdk-8
和 jdk-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的特性。