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 是什么:一次性流水线,不是数据结构

三个核心认知:

  1. 不存数据:Stream 只是数据源上的一层"视图 + 加工步骤",操作完数据还在源里;
  2. 不改源:所有中间操作返回新 Stream,源集合原样不动(函数式味道);
  3. 惰性执行 + 一次性:中间操作只是"记账",遇到终端操作才一次性拉通;流水线跑完即作废,重用报 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 公共池跑。三个使用前提(不满足就别用):

  1. 数据量够大(小数据并行调度的开销超过收益);
  2. 源易切分(ArrayList/数组好切,LinkedList/iterate 差);
  3. 操作本身无状态、无副作用、无 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 替身

六、高频面试题速答

  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()

勾连


⬅️ 07-Lambda与函数式接口 🏠 00-Java ➡️ 09-IO流体系