1. 概述
本文深入探讨 Java Stream API 的不同使用方式如何影响数据的生成、处理和收集顺序。
同时也会分析 顺序性对性能的影响,特别是在并行流中的表现。✅
掌握这些细节,能帮你避免一些“看似正确却踩坑”的逻辑问题,尤其是在处理高并发数据流时。
2. 遇到顺序(Encounter Order)
所谓“遇到顺序”(Encounter Order),指的是 Stream 遍历数据源时元素出现的原始顺序。这个顺序是否保留,取决于数据源本身是否有内在排序。
2.1 集合数据源的遇到顺序
不同类型的集合作为 Stream 源,其 encounter order 表现不同。
比如 List
是有序集合,而 TreeSet
虽然有序但会按自然排序重排元素,HashSet
则完全无序。
来看一个直观的例子:
@Test
public void givenTwoCollections_whenStreamedSequentially_thenCheckOutputDifferent() {
List<String> list = Arrays.asList("B", "A", "C", "D", "F");
Set<String> set = new TreeSet<>(list);
Object[] listOutput = list.stream().toArray();
Object[] setOutput = set.stream().toArray();
assertEquals("[B, A, C, D, F]", Arrays.toString(listOutput));
assertEquals("[A, B, C, D, F]", Arrays.toString(setOutput));
}
✅ 结果说明:
List
保留了插入顺序,Stream 输出一致。TreeSet
自动排序,破坏了原始 encounter order。
⚠️ 关键点:只要 Stream 源本身是有序的(如 List),无论串行还是并行处理,Stream 实现都会尽力维持 encounter order。
验证一下并行流的情况:
@Test
public void givenTwoCollections_whenStreamedInParallel_thenCheckOutputDifferent() {
List<String> list = Arrays.asList("B", "A", "C", "D", "F");
Set<String> set = new TreeSet<>(list);
Object[] listOutput = list.stream().parallel().toArray();
Object[] setOutput = set.stream().parallel().toArray();
assertEquals("[B, A, C, D, F]", Arrays.toString(listOutput));
assertEquals("[A, B, C, D, F]", Arrays.toString(setOutput));
}
结果不变 —— 并行流也保持了 List
的 encounter order。
2.2 显式移除顺序:unordered()
你可以通过调用 unordered()
方法 显式解除 Stream 的顺序约束,从而可能提升并行处理性能。
示例:
Set<Integer> set = new TreeSet<>(
Arrays.asList(-9, -5, -4, -2, 1, 2, 4, 5, 7, 9, 12, 13, 16, 29, 23, 34, 57, 102, 230));
不调用 unordered()
:
set.stream().parallel().limit(5).toArray();
输出(保持 TreeSet 的自然排序):
[-9, -5, -4, -2, 1]
显式调用 unordered()
:
set.stream().unordered().parallel().limit(5).toArray();
输出可能变为:
[1, 4, 7, 9, 23]
⚠️ 原因分析:
- 串行流中
unordered()
影响很小,因为本就是单线程处理。 - 并行流中,
unordered()
允许各线程自由处理数据块,不再等待前一个元素完成,从而打破顺序,但也可能提高吞吐量。
3. 中间操作对顺序的影响
中间操作(Intermediate Operations)有些会保持顺序,有些则会改变。
操作 | 是否影响顺序 | 说明 |
---|---|---|
filter , map , peek |
✅ 保持顺序 | 只是转换或筛选,不打乱原有顺序 |
sorted() |
❌ 改变顺序 | 显式排序,重新定义 encounter order |
unordered() |
❌ 移除顺序 | 解除顺序约束 |
distinct() |
⚠️ 依赖顺序 | 有序流中需维护顺序,性能受影响 |
示例:sorted()
改变顺序
@Test
public void givenUnsortedStreamInput_whenStreamSorted_thenCheckOrderChanged() {
List<Integer> list = Arrays.asList(-3, 10, -4, 1, 3);
Object[] listOutput = list.stream().toArray();
Object[] listOutputSorted = list.stream().sorted().toArray();
assertEquals("[-3, 10, -4, 1, 3]", Arrays.toString(listOutput));
assertEquals("[-4, -3, 1, 3, 10]", Arrays.toString(listOutputSorted));
}
✅ 小结:大多数中间操作保持 encounter order,但 sorted()
和 unordered()
是明确改变顺序的例外。
4. 终端操作对顺序的影响
终端操作(Terminal Operations)也可能影响最终结果的顺序,尤其是收集类操作。
4.1 forEach
vs forEachOrdered
这两个方法看似功能相同,但关键区别在于:
forEach
:不保证顺序,各线程独立执行。forEachOrdered
:强制保持 encounter order,前一个元素处理完才处理下一个。
示例:
List<String> list = Arrays.asList("B", "A", "C", "D", "F");
使用 forEachOrdered
:
list.stream().parallel().forEachOrdered(e -> logger.log(Level.INFO, e));
输出(有序):
INFO: B
INFO: A
INFO: C
INFO: D
INFO: F
使用 forEach
:
list.stream().parallel().forEach(e -> logger.log(Level.INFO, e));
输出(无序):
INFO: C
INFO: F
INFO: B
INFO: D
INFO: A
⚠️ 踩坑提醒:在并行流中使用 forEach
打日志,日志顺序混乱是常态,不要指望它有序!
4.2 collect
操作与集合类型
最终收集到的集合类型决定了结果是否有序。
例如,使用 TreeSet
这种自然排序的集合,会破坏原始顺序:
@Test
public void givenSameCollection_whenStreamCollected_checkOutput() {
List<String> list = Arrays.asList("B", "A", "C", "D", "F");
List<String> collectionList = list.stream().parallel().collect(Collectors.toList());
Set<String> collectionSet = list.stream().parallel()
.collect(Collectors.toCollection(TreeSet::new));
assertEquals("[B, A, C, D, F]", collectionList.toString());
assertEquals("[A, B, C, D, F]", collectionSet.toString());
}
✅ 结论:toList()
保持顺序,toCollection(TreeSet::new)
按自然排序重排。
4.3 如何保持收集时的顺序?
如果你用 Collectors.toMap()
,默认生成 HashMap
,不保证顺序。
示例:
@Test
public void givenList_whenStreamCollectedToHashMap_thenCheckOrderChanged() {
List<String> list = Arrays.asList("A", "BB", "CCC");
Map<String, Integer> hashMap = list.stream().collect(Collectors
.toMap(Function.identity(), String::length));
Object[] keySet = hashMap.keySet().toArray();
assertEquals("[BB, A, CCC]", Arrays.toString(keySet));
}
✅ 解决方案:使用 LinkedHashMap
保持插入顺序。
通过 toMap
的四参数版本指定 Supplier
:
@Test
public void givenList_whenCollectedtoLinkedHashMap_thenCheckOrderMaintained(){
List<String> list = Arrays.asList("A", "BB", "CCC");
Map<String, Integer> linkedHashMap = list.stream().collect(Collectors.toMap(
Function.identity(),
String::length,
(u, v) -> u, // merge function
LinkedHashMap::new // supplier
));
Object[] keySet = linkedHashMap.keySet().toArray();
assertEquals("[A, BB, CCC]", Arrays.toString(keySet));
}
✅ 简单粗暴:想保持顺序?用 LinkedHashMap
就对了。
5. 顺序对性能的影响
顺序性对串行流影响不大,但对并行流性能影响显著。
原因:有序并行流需要协调线程间顺序,导致线程必须等待前一个元素处理完成,丧失并行优势。
我们用 JMH 做两个 benchmark 对比。
5.1 distinct()
操作性能对比
@Benchmark
public void givenOrderedStreamInput_whenStreamDistinct_thenShowOpsPerMS() {
IntStream.range(1, 1_000_000).parallel().distinct().toArray();
}
@Benchmark
public void givenUnorderedStreamInput_whenStreamDistinct_thenShowOpsPerMS() {
IntStream.range(1, 1_000_000).unordered().parallel().distinct().toArray();
}
结果:
Benchmark Mode Cnt Score Error Units
TestBenchmark.givenOrdered... avgt 2 222252.283 us/op
TestBenchmark.givenUnordered... avgt 2 78221.357 us/op
✅ 无序流快了近 3 倍! 因为 distinct
在无序流中可以并行去重,无需维护顺序。
5.2 filter()
操作性能对比
@Benchmark
public void givenOrderedStreamInput_whenStreamFiltered_thenShowOpsPerMS() {
IntStream.range(1, 100_000_000).parallel().filter(i -> i % 10 == 0).toArray();
}
@Benchmark
public void givenUnorderedStreamInput_whenStreamFiltered_thenShowOpsPerMS(){
IntStream.range(1,100_000_000).unordered().parallel().filter(i -> i % 10 == 0).toArray();
}
结果:
Benchmark Mode Cnt Score Error Units
TestBenchmark.givenOrdered... avgt 2 116333.431 us/op
TestBenchmark.givenUnordered... avgt 2 111471.676 us/op
✅ 差距很小。因为 filter
本身不依赖顺序,影响主要来自 unordered()
对数据分片的优化。
⚠️ 结论:对于 distinct
、sorted
等依赖顺序的操作,并行流中显式调用 unordered()
可大幅提升性能。
6. 总结
- ✅ Encounter order 来源于数据源:
List
有序,TreeSet
重排序,HashSet
无序。 - ✅ 中间操作多数保持顺序,
sorted()
和unordered()
是例外。 - ✅ 终端操作如
forEach
、collect
是否保持顺序,取决于具体方法和目标集合类型。 - ✅ 并行流中,顺序约束会显著影响性能,尤其是
distinct
类操作。 - ✅ 必要时用
unordered()
解除顺序,可提升并行处理吞吐量。
示例代码已上传至 GitHub:https://github.com/yourname/java-stream-ordering-demo