Stream API的核心价值是用声明式写法替代循环嵌套,学习关键是从真实小任务出发:创建流→链式中间操作(惰性求值)→终端操作触发执行,重点理解三类操作的行为差异及Collectors的实用聚合能力。

直接上手练,别先背概念。Stream API 的核心价值是“用声明式写法替代循环嵌套”,学得快的关键在于:从真实小任务出发,边写边理解流水线怎么搭、什么时候真正执行、结果怎么收。
从一个具体需求开始写起
比如处理一组字符串:List<string> names = Arrays.asList("alice", "bob", "charlie", "dave");</string>,目标是“找出长度大于 3 的名字,转成大写,去重,按字母排序,取前 2 个”。不用 Stream 时容易写成带临时 List 和 for 循环的代码;用 Stream 就是一行链式调用:
-
names.stream()—— 创建流 -
.filter(s -> s.length() > 3)—— 中间操作:只留长度超 3 的 -
.map(String::toUpperCase)—— 中间操作:每个转大写 -
.distinct()—— 中间操作:去重(如果数据有重复) -
.sorted()—— 中间操作:自然排序 -
.limit(2)—— 中间操作:只取前两个 -
.collect(Collectors.toList())—— 终端操作:收集成新 List
这整条链不会立刻运行,直到 collect 被调用才真正遍历一次数据。这种“惰性求值”是 Stream 高效的基础,也是初学者最容易忽略的点。
重点分清三类操作的实际行为
不是记住名词,而是搞懂每类操作在代码里“干了什么、能不能再用、会不会立刻跑”:
立即学习“Java免费学习笔记(深入)”;
-
创建流:只是拿到一个数据视图,不读数据也不复制。比如
list.stream()不改变原 list,Files.lines(path)也只在终端操作触发时才打开文件 -
中间操作:全都不执行,只记下“下一步要做什么”。可以无限链下去,但每调一次都返回新流——旧流不能复用,否则报
IllegalStateException -
终端操作:一调就触发整个流水线,执行完流就“废了”。常见如
collect、count、findFirst、anyMatch。其中anyMatch这类短路操作,找到第一个匹配就停,适合大数据源
用 Collectors 解决实际聚合问题
收集不是终点,而是把流变成你需要的结构。Collectors 提供的工具远不止 toList():
- 转成 Map:
collect(Collectors.toMap(String::toLowerCase, s -> s.length())),注意 key 冲突会抛异常,可用toMap(..., (a,b) -> a)指定冲突策略 - 分组:
collect(Collectors.groupingBy(s -> s.length()))→ 按长度分组,结果是Map<integer list>></integer> - 分区(只有 true/false 两类):
collect(Collectors.partitioningBy(s -> s.startsWith("a"))) - 拼字符串:
collect(Collectors.joining(", ")),比手动加逗号安全得多
这些不是炫技,而是替代传统 for + if + map.put 的标准写法,越早用越顺手。
并行流别急着开,先测再换
list.parallelStream() 看似一键加速,但实际效果取决于场景:
- 数据量小(比如几百个元素),并行反而慢——线程调度和合并结果的开销盖过了计算收益
- 操作本身很轻(如
s.length()),并行意义不大;换成解析 JSON 或正则匹配这类耗时操作,才可能明显提速 - 确保中间操作无状态:别在
filter里改共享变量,别用peek做计数,否则结果不可靠
建议起步阶段一律用 stream(),等逻辑稳定后,拿 JMH 做简单压测,再决定是否切并行。


















