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() 对数据分片的优化。

⚠️ 结论:对于 distinctsorted 等依赖顺序的操作,并行流中显式调用 unordered() 可大幅提升性能。

6. 总结

  • Encounter order 来源于数据源List 有序,TreeSet 重排序,HashSet 无序。
  • 中间操作多数保持顺序sorted()unordered() 是例外。
  • 终端操作如 forEachcollect 是否保持顺序,取决于具体方法和目标集合类型
  • 并行流中,顺序约束会显著影响性能,尤其是 distinct 类操作。
  • 必要时用 unordered() 解除顺序,可提升并行处理吞吐量。

示例代码已上传至 GitHub:https://github.com/yourname/java-stream-ordering-demo


原始标题:Stream Ordering