Stream与Optional
本篇定位
Lambda 落地后的两大日常工具:Stream 把集合加工写成声明式流水线(做什么,而不是怎么循环),Optional 把"可能没有"写进类型签名(逼调用方处理空值)。两者都是 JDK 8 引入、日常代码出现率最高的新特性,也是 07-Lambda与函数式接口 的最大用户。
一、问题提出:两层循环 + 临时集合的样板代码
需求:"从订单列表里挑出金额大于 100 的,取客户名去重,取前 10 个"。命令式写法:
List<String> result = new ArrayList<>();
Set<String> seen = new HashSet<>();
for (Order o : orders) {
if (o.getAmount() > 100) {
if (seen.add(o.getCustomerName())) {
result.add(o.getCustomerName());
if (result.size() == 10) break;
}
}
}
业务被淹没在"怎么遍历、怎么去重、怎么截断"的机械步骤里。Stream 写法:
List<String> result = orders.stream()
.filter(o -> o.getAmount() > 100)
.map(Order::getCustomerName)
.distinct()
.limit(10)
.collect(Collectors.toList());
一行主语(数据源)+ 一串动词(加工步骤),每步意图自明。
二、Stream 是什么:一次性流水线,不是数据结构
三个核心认知:
- 不存数据:Stream 只是数据源上的一层"视图 + 加工步骤",操作完数据还在源里;
- 不改源:所有中间操作返回新 Stream,源集合原样不动(函数式味道);
- 惰性执行 + 一次性:中间操作只是"记账",遇到终端操作才一次性拉通;流水线跑完即作废,重用报
IllegalStateException。
Stream<Order> s = orders.stream().filter(o -> o.getAmount() > 100); // 记账,未执行
List<Order> big = s.collect(Collectors.toList()); // 此刻才真正遍历
// s.collect(...); // ❌ IllegalStateException: stream has already been operated upon
中间操作(惰性):filter / map / flatMap / sorted / distinct / limit / skip / peek;
终端操作(触发):collect / forEach / reduce / count / anyMatch / allMatch / findFirst / toArray。
map vs flatMap 一句话:map 一进一出(元素换形态),flatMap 一进多出(把每个元素摊平成流再拼起来)——flatMap(o -> o.getItems().stream()) 把"每个订单的多个商品"摊成一条商品流。
三、collect 与收集器:终端操作的瑞士军刀
// 按客户分组计数
Map<String, List<Order>> byCustomer =
orders.stream().collect(Collectors.groupingBy(Order::getCustomerName));
// 分组 + 下游统计:每个客户消费总额
Map<String, Double> amountByCustomer = orders.stream()
.collect(Collectors.groupingBy(Order::getCustomerName,
Collectors.summingDouble(Order::getAmount)));
// 分区(二分组)、拼接、求和、平均
Map<Boolean, List<Order>> partitioned =
orders.stream().collect(Collectors.partitioningBy(o -> o.getAmount() > 100));
String names = orders.stream().map(Order::getCustomerName)
.collect(Collectors.joining(", ", "[", "]"));
double avg = orders.stream().collect(Collectors.averagingDouble(Order::getAmount));
JDK 16+ 日常收集直接 .toList()(不可变,比 collect(Collectors.toList()) 简洁且明确不可改)。
四、数值流与并行流
装箱是 Stream 的隐形税:Stream<Integer> 每个元素都是对象。数值聚合走原始类型特化流:
int total = orders.stream()
.mapToInt(Order::getAmount) // IntStream:无装箱
.sum();
并行流:orders.parallelStream() 把流水线切给 ForkJoinPool 公共池跑。三个使用前提(不满足就别用):
- 数据量够大(小数据并行调度的开销超过收益);
- 源易切分(
ArrayList/数组好切,LinkedList/iterate差); - 操作本身无状态、无副作用、无 IO(有 IO 或锁就退化甚至出错)。
CPU 密集 + 大集合 + 纯计算时再考虑,默认串行是更稳的选择。
五、Optional:把 null 写进类型系统
null 的原罪:类型签名 Order findOrder(String id) 看不出"可能没有",忘了判空就 NPE。Optional 把这份信息显式化:
public Optional<Order> findOrder(String id) {
return repository.query(id); // 有值/无值都合法,签名自明
}
// 调用方被"逼"着面对空值
findOrder(id)
.map(Order::getCustomer)
.map(Customer::getName)
.orElse("未知客户");
// 也可以:有则处理,无则抛业务异常
Order order = findOrder(id).orElseThrow(() -> new BusinessException("订单不存在"));
正确姿势:作为方法返回类型表达"可能没有";链式 map/orElse 消解嵌套判空。
错误姿势(面试爱挖的坑):
optional.get(); // ❌ 等于换了件马甲的 null 检查,裸 get 易抛 NoSuchElementException
if (optional != null) // ❌ Optional 本身永远不该为 null,直接用 isPresent 判内容
optional.ifPresent(o -> map.put(k, o)); // ⚠️ 可以,但 Optional 做字段/参数会污染 API——
// 字段用普通引用+判空,入参用重载表达
一句话:Optional 是返回值的包装,不是万能 null 替身。
六、高频面试题速答
- Stream 中间操作为什么惰性? 链式记账后一次遍历融合执行,短路由
limit提前终止——比手写多层循环少跑趟数。 - map 与 flatMap 区别? 一进一出 vs 一进多出摊平嵌套结构。
- 并行流怎么实现的?默认线程池多大? ForkJoinPool.commonPool,并行度 = CPU 核数 - 1;可通过系统属性调,但全局共享(这也是别乱用的原因之一)。
- Optional.get 不判空会怎样? 抛
NoSuchElementException;生产代码用orElse/orElseGet/orElseThrow。 - orElse 与 orElseGet 区别?
orElse(默认值)参数无论有无值都求值;orElseGet(Supplier)惰性,默认值构造昂贵时必用后者。 - Stream 复用报什么错?
IllegalStateException——流水线一次性,重取数据重新stream()。
勾连
- Lambda 与四大函数式接口:07-Lambda与函数式接口
- ForkJoin 池与线程池参数:03-线程池
- collect 分组与 Map 计数套路的容器基础:00-Map总览
- NPE 业务异常处理:04-异常体系
⬅️ 07-Lambda与函数式接口 🏠 00-Java ➡️ 09-IO流体系
💬 评论