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