很多开发者第一次使用 Java 8 的 Stream.sorted(...) 方法时,会以为它能直接对原始集合进行排序。但实际运行后却发现——排序完全没有生效
来看看下面这段真实代码:
 

🚧 初学者常见误区:为何 sorted() 不起作用?

sorted 明明调用了,为何集合没有变?结果也没有排序?
这是因为 Stream.sorted() 是一个 中间操作(intermediate operation)具有惰性(lazy)特性
只有当你调用类似 collect()forEach()count() 这样的 终端操作(terminal operation) 时,整个 Stream 才会开始处理数据。
 
典型修正如下:
只有出现 collect(...) / forEach(...) / count() / findFirst() 等终端操作时,Stream 才会开始把源数据通过各个中间阶段“串起来”并执行。
 
Oracle 官方文档明确指出:sorted有状态的中间操作(stateful intermediate operation),且中间操作本身只构建管线,不会立刻处理元素。见:https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html
To perform a computation, stream operations are composed into a stream pipeline. A stream pipeline consists of a source (which might be an array, a collection, a generator function, an I/O channel, etc), zero or more intermediate operations (which transform a stream into another stream, such as filter(Predicate)), and a terminal operation (which produces a result or side-effect, such as count() or forEach(Consumer)). Streams are lazy; computation on the source data is only performed when the terminal operation is initiated, and source elements are consumed only as needed.
 

🤗 中间操作与终端操作

  • 中间操作(Intermediate):返回一个新的 Stream惰性,仅描述转换,不立刻执行。例如:map / filter / distinct / sorted / limit / skip / peek
  • 终端操作(Terminal):触发管线求值并产生非 Stream 结果(集合、标量或副作用),如:collect / reduce / forEach / count / min / max / anyMatch / findFirst / toArray
Java 官方包级文档将 Stream 描述为以流水线(pipeline)方式执行的函数式聚合操作;示例与概念以“流水线 + 终端触发”为核心。
从源码层面看,终端操作会调用管线头部的 AbstractPipeline#evaluate(...),顺序或并行地驱动整条流水线,并在此时消费数据源
 

😶 底层实现差异

sorted(中间操作)和 collect()(终端操作)为例。

sorted:有状态中间操作如何实现

  • JDK 把排序实现封装在 java.util.stream.SortedOps 中,对引用流的实现类是 SortedOps.OfRef,它继承自 ReferencePipeline.StatefulOp有状态)。
  • 顺序执行时,sorted 会创建一个SortingSink,把上游元素先缓冲到数组或 ArrayList结束时一次性排序并下推到下游 Sink。
  • 并行执行时,它会先把数据收集到数组,然后使用 Arrays.parallelSort 排序,再继续下推。
这些行为都可以在 SortedOps 源码中直接看到:
  • opWrapSink(...) 根据流是否 SIZED 选择不同的 SortingSink
  • opEvaluateParallel(...) 中为并行情况执行两段式(收集→并行排序)。

collect():终端操作如何驱动管线与归约

  • collect终端操作,其核心由 ReduceOps.makeRef(Collector) 生成一个 TerminalOp
  • TerminalOp 会创建 ReducingSink,在消费元素时反复调用 Collectorsupplier / accumulator / combiner 完成可变归约(mutable reduction)
  • 终端阶段通过 AbstractPipeline#evaluate(...) 驱动整个管线执行:顺序时走 evaluateSequential,并行时走 evaluateParallel;数据在各级 Sink 间“推送”,最终由 ReducingSink 聚合为结果。
 
对比要点:
  • sorted:中间、惰性、有状态、需要缓冲与排序,不产出结果
  • collect终端、立即触发求值、基于 Collectorsupplier/accumulator/combiner 进行归约,产出最终结果或副作用。
 

🤔 深入理解 Stream 的设计理念

官方文档给出了 Stream 的几个关键设计点:
  1. 流水线(Pipeline)模型:把多个中间操作链接成一条描述性的变换链;最终由终端操作触发求值。
  1. 惰性求值(Laziness):中间操作不立刻执行,允许 JDK 在终端执行时进行整体优化与融合(fusion)。源码注释中也说明:只有在终端操作开始时,才会消费源数据。
  1. 有状态 vs 无状态:像 map/filter 等是无状态,逐元素处理即可;sorted/distinct 等是有状态,需要观察多元素才能产生正确输出,因此常见到缓冲与二段式并行策略。sorted 的 Javadoc 明确为stateful
  1. 顺序与并行的统一抽象:相同的流水线描述可在顺序或并行模式下执行;并行时,终端操作会调用并行的评估路径(如 evaluateParallel),在内部使用 Spliterator、分治与并行收集/排序等策略。
这些理念共同支撑了 Stream 在表达力、可读性与性能潜力之间的平衡。

😎 速查表:常见中间/终端操作(Java 8)

说明:以下仅选取常用方法;是否“有状态/短路”指该操作本身的特性(有助于推断性能与内存行为)。

中间操作(Intermediate)

方法
类型
是否有状态
是否可短路
作用/备注
map, mapToInt/Long/Double
无状态
映射转换
flatMap, flatMapTo*
无状态
一对多展开后再扁平化
filter
无状态
条件保留
peek
无状态
调试/旁路观察(无副作用更佳)
distinct
有状态
去重,需要记忆已见元素
sorted() / sorted(Comparator)
有状态
全局排序,需缓冲后排序(Javadoc 标明 stateful)Oracle 文档
limit(n)
有状态
截断前 n 个元素(短路)
skip(n)
有状态
跳过前 n 个元素
unordered()
无状态
放弃遇迭顺序提示优化

终端操作(Terminal)

方法
是否短路
结果类型
备注
collect(Collector)
集合/自定义结果
可变归约(Collectors 族)Oracle 文档
reduce(...)
标量或可选值
不可变归约
forEach / forEachOrdered
void
消费元素、有副作用
toArray() / toArray(IntFunction)
数组
收集为数组
count()
否(有时可优化)
long
计数
min / max
Optional<T>
比较得到极值
findFirst / findAny
Optional<T>
短路查找
anyMatch / allMatch / noneMatch
boolean
谓词匹配,短路

🤠 小结与实践建议

✅ Stream 三大运行原则

  1. 中间操作不会触发执行
  1. 只有终端操作才会驱动执行
  1. 有状态操作(如 sorted)需要缓冲或排序,执行成本高

🧪 实战建议

  • 💡 不要假设中间操作会有副作用(sorted 不会排序原集合)
  • 💡 每写一个 stream(),就思考终端操作在哪里
  • 💡 多用 .collect(...) 把结果收集成新集合,避免混淆原集合
 

🙃 参考文章

 
 
小米 CR6608 刷 OpenWrt 全流程实录K8s + Spring Cloud + Druid + MySQL 环境下的 Communications Link Failure 问题全解析
Loading...