学完这篇你能拿到一套可以直接抄进项目里的写法:用 Stream 把 for 循环嵌套的集合处理压成一条链,用 Optional 把「可能为空」的返回值处理干净,并且知道哪些地方用了反而更糟。
下面用一个订单列表示例贯穿全文,先定义数据结构(JDK 16+ 用 record,JDK 8 换成普通类加 getter):
public record Order(String id, String userId, String status,
BigDecimal amount, List<String> tags) {}
第一步:认清 Stream 的三段式结构
任何 Stream 操作都能拆成三段,缺一不可:
List<String> ids = orders.stream() // 1. 数据源
.filter(o -> "PAID".equals(o.status())) // 2. 中间操作(可 0~N 个)
.map(Order::id) // 中间操作
.collect(Collectors.toList()); // 3. 终止操作(必须有)
中间操作是惰性的,只做登记,不执行。少了终止操作,整条链一条语句都不会跑。
注意:一个 Stream 只能消费一次。写 Stream s = list.stream(); s.count(); s.collect(...) 会直接抛 IllegalStateException,要重新 list.stream() 开一条。
第二步:把 for 循环翻译成常用的四招
过滤 + 映射就是上面的 filter + map。
分组替代「遍历 + 判断 key + put 进 Map」:
Map<String, List<Order>> byUser =
orders.stream().collect(Collectors.groupingBy(Order::userId));
求和用 reduce,金额是 BigDecimal 时别用 summingDouble,会有精度问题:
BigDecimal total = orders.stream()
.filter(o -> "PAID".equals(o.status()))
.map(Order::amount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
排序取前 N:
List<Order> top10 = orders.stream()
.sorted(Comparator.comparing(Order::amount).reversed())
.limit(10)
.collect(Collectors.toList());
第三步:flatMap 拍平嵌套,mapToInt 避免装箱
标签是 List<List<String>> 时,用 map 得到的是流中流,必须 flatMap:
List<String> allTags = orders.stream()
.flatMap(o -> o.tags().stream())
.distinct()
.collect(Collectors.toList());
需要求和 / 平均值时改用数值流,直接拿到 int / double,省掉 Integer 拆装箱:
double avgTags = orders.stream().mapToInt(o -> o.tags().size()).average().orElse(0);
注意:average()、max()、findFirst() 返回的都是 Optional,要么 orElse 兜底,要么 ifPresent 处理,不要直接 .get()。
第四步:Optional 只用在对的四个位置
Optional 的设计意图是「方法返回值可能没有」,所以它适合用在返回值上,写起来就四板斧:
// 1. 取值带默认
String name = userRepo.findById(id).map(User::name).orElse("匿名");
// 2. 有值才执行
userRepo.findById(id).ifPresent(u -> log.info("命中用户 {}", u.name()));
// 3. 没有就抛业务异常
User u = userRepo.findById(id)
.orElseThrow(() -> new IllegalArgumentException("用户不存在: " + id));
// 4. 链式安全取值(中间任意一环为空,整条链短路成 empty)
String city = userRepo.findById(id)
.map(User::address)
.map(Address::city)
.orElse("未知");
注意:orElse 和 orElseGet 的区别不是风格问题。orElse(createDefault()) 无论有没有值,createDefault() 都会先执行一遍——里面要是有查库或建对象,就是白白浪费。有副作用的兜底一律用 orElseGet(() -> createDefault())。
注意:Optional 不要当字段类型、方法参数、集合元素用。它不实现 Serializable,塞进实体类会带来序列化麻烦;当参数用则强迫调用方每次都包一层 Optional.ofNullable,得不偿失。
第五步:两者配合的一个完整实战
需求:找出金额最大的订单,输出「用户:金额」,没有订单时给默认文案。
String label = orders.stream()
.max(Comparator.comparing(Order::amount)) // Optional<Order>
.map(o -> o.userId() + ":" + o.amount())
.orElse("暂无订单");
注意这里 max 给的是 Optional,map 在空值上自动短路,最后 orElse 兜底,全程没有一行 if (x != null)。
第六步:调试与并行的两个坑
链式调用出问题时,中间插一个 peek(System.out::println) 看数据流经每一步的样子。但 peek 只是调试用,不要在里面写业务逻辑——JDK 9 之后对 count() 之类做了短路优化,peek 可能根本不执行。
parallelStream() 默认走 ForkJoinPool.commonPool(),线程数是 CPU 核数减一,全 JVM 共享。里面做数据库查询、HTTP 调用会把公共池占满,影响其他并行任务。集合处理是纯 CPU 计算且数据量大(万级以上)才考虑并行,IO 场景老老实实用普通流。
小结
- Stream 三段式:数据源 → 中间操作(惰性)→ 终止操作,缺终止操作则什么都不执行。
- 一个流只能消费一次,重复使用要重新
stream()。
- 分组用
groupingBy,聚合用 reduce,嵌套拍平用 flatMap,数值计算用 mapToInt。
- BigDecimal 求和不走
summingDouble,精度会丢。
- Optional 只用在返回值,四板斧:
orElse / ifPresent / orElseThrow / map 链式。
- 有副作用的兜底必须用
orElseGet,orElse 会无条件求值。
- 别把 Optional 当字段、参数、集合元素;
parallelStream 避开 IO 场景。