Java Stream 流全面实战指南(Spring Boot 3 + JDK 17)

Java Stream 流全面实战指南(Spring Boot 3 + JDK 17)

本文面向使用 Spring Boot 3 + JDK 17 的一线开发同学,系统梳理 Java Stream API 的全部用法:流的创建、中间操作、终止操作、Collectors 收集器、原始类型流、Optional 联动、并行流,以及 JDK 9~17 陆续加入的 Stream 新特性。每个知识点都配有可直接运行的示例,并结合 Spring Boot 项目中的真实场景(DTO 转换、分组聚合、链式查询等)给出实战写法与避坑指南。


目录

  1. [Stream 是什么](#Stream 是什么)
  2. [创建流的 12 种方式](#创建流的 12 种方式)
  3. 中间操作
  4. 终止操作
  5. [Collectors 收集器全解](#Collectors 收集器全解)
  6. [原始类型流:IntStream / LongStream / DoubleStream](#原始类型流:IntStream / LongStream / DoubleStream)
  7. [Stream 与 Optional 的配合](#Stream 与 Optional 的配合)
  8. 并行流
  9. [JDK 9~17 的 Stream 新特性汇总](#JDK 9~17 的 Stream 新特性汇总)
  10. [Spring Boot 项目实战场景](#Spring Boot 项目实战场景)
  11. 常见坑与最佳实践

一、Stream 是什么

Stream(流)是 JDK 8 引入的、用于对集合进行函数式聚合操作的抽象。它不是集合本身,不存储数据,而是描述"对数据源做什么计算"。

一个典型的流操作分三步:

复制代码
数据源 → 中间操作(链式、惰性) → 终止操作(触发实际计算)
java 复制代码
List<String> names = users.stream()          // 1. 创建流
        .filter(u -> u.getAge() > 18)        // 2. 中间操作:过滤(惰性,暂不执行)
        .map(User::getName)                    //    中间操作:映射
        .sorted()                              //    中间操作:排序
        .toList();                             // 3. 终止操作:此时才真正执行整条链

流的核心特性

特性 说明
不存储数据 只描述计算过程,数据仍来自集合/数组等数据源
惰性求值 中间操作不会立即执行,直到遇到终止操作
一次性消费 流只能被消费一次,终止后不能复用
支持并行 可轻松切换为并行执行(parallel()
不修改源 操作产生新结果,不改动原集合

什么时候用 Stream:过滤、转换、聚合、分组、去重、拼接、统计------凡是"对一批数据做加工再收集结果"的场景,都是 Stream 的主场。在 Spring Boot 的 Service 层,它能替代大量手写 for 循环,代码更声明式、更短。


二、创建流的 12 种方式

2.1 从集合创建(最常用)

java 复制代码
List<User> users = userRepository.findAll();

Stream<User> stream1 = users.stream();           // 串行流
Stream<User> stream2 = users.parallelStream();   // 并行流(慎用,见第八章)

2.2 从数组创建

java 复制代码
String[] arr = {"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
Stream<String> sub    = Arrays.stream(arr, 0, 2);   // 指定下标区间 [0, 2)

2.3 Stream.of()

java 复制代码
Stream<String> stream = Stream.of("a", "b", "c");
Stream<Integer> nums  = Stream.of(1, 2, 3, 4, 5);

2.4 Stream.iterate():生成有规律的序列

java 复制代码
// JDK 8 写法:无限流,必须配合 limit
Stream.iterate(0, n -> n + 2).limit(5).forEach(System.out::println);   // 0 2 4 6 8

// JDK 9 写法:带终止条件,自动结束
Stream.iterate(1, n -> n < 100, n -> n * 2)
      .forEach(System.out::println);   // 1 2 4 8 16 32 64

2.5 Stream.generate():自定义供应

java 复制代码
// 5 个随机数
Stream.generate(Math::random).limit(5).forEach(System.out::println);

// 5 个 UUID
Stream.generate(UUID::randomUUID).limit(5).forEach(System.out::println);

2.6 IntStream / LongStream / DoubleStream.range()

java 复制代码
IntStream.range(1, 6)        // 1,2,3,4,5(不含 6)
         .forEach(System.out::println);

IntStream.rangeClosed(1, 5)  // 1,2,3,4,5(含 5)
         .forEach(System.out::println);

2.7 Stream.concat():合并两个流

java 复制代码
List<User> activeUsers   = repo.findActive();
List<User> disabledUsers = repo.findDisabled();

Stream<User> all = Stream.concat(activeUsers.stream(), disabledUsers.stream());

2.8 Stream.empty()

java 复制代码
Stream<User> empty = Stream.empty();   // 常用于方法返回空流,避免返回 null

2.9 Stream.ofNullable()(JDK 9)

java 复制代码
// 值不为 null → 单元素流;为 null → 空流。处理可空数据非常优雅
String nickName = user.getNickName();
Stream.ofNullable(nickName)
      .forEach(name -> System.out.println("昵称:" + name));

2.10 从文件/目录创建(Files / BufferedReader)

java 复制代码
// 按行读取文件
try (Stream<String> lines = Files.lines(Path.of("/data/app.log"))) {
    lines.filter(line -> line.contains("ERROR"))
         .forEach(System.out::println);
}

// BufferedReader 也支持
try (BufferedReader reader = Files.newBufferedReader(path)) {
    reader.lines().count();
}

// 遍历目录(walk 递归,list 只列一层)
try (Stream<Path> paths = Files.walk(Path.of("/data"))) {
    paths.filter(p -> p.toString().endsWith(".log")).forEach(System.out::println);
}

2.11 从字符串按分隔符创建(Pattern)

java 复制代码
Pattern.compile(",")
       .splitAsStream("a,b,c,d")
       .forEach(System.out::println);   // a b c d

2.12 Random.ints() 等

java 复制代码
new Random().ints(5, 1, 101)    // 5 个 1~100 的随机整数
            .forEach(System.out::println);

三、中间操作

中间操作返回一个新的流,可以无限链式调用,且都是惰性的。

3.1 filter():过滤

java 复制代码
// 留下年龄大于 18 且状态正常的用户
List<User> result = users.stream()
        .filter(u -> u.getAge() > 18)
        .filter(u -> u.getStatus() == UserStatus.ACTIVE)
        .toList();

3.2 map():一对一转换

java 复制代码
// 提取所有用户名
List<String> names = users.stream()
        .map(User::getName)
        .toList();

// DTO 转换(Spring Boot 中最常见,详见第十章)
List<UserVO> vos = users.stream()
        .map(this::toVO)
        .toList();

// 字符串转大写
List<String> upper = codes.stream().map(String::toUpperCase).toList();

3.3 flatMap():一对多"拍平"转换

map 是每个元素转换成一个元素;flatMap 是每个元素转换成一个,再把所有流合并成一个流。

java 复制代码
// List<List<String>> 拍平成 List<String>
List<List<String>> nested = List.of(List.of("a", "b"), List.of("c", "d"));
List<String> flat = nested.stream()
        .flatMap(List::stream)
        .toList();   // [a, b, c, d]

// 按空格拆词并去重统计
String sentence = "java stream java spring";
List<String> words = Arrays.stream(sentence.split(" "))
        .distinct()
        .toList();   // [java, stream, spring]

// 提取每个订单的所有商品,拍平成商品流
List<Product> allProducts = orders.stream()
        .flatMap(order -> order.getItems().stream())
        .toList();

3.4 distinct():去重

java 复制代码
// 基于 equals()/hashCode() 去重
List<Integer> unique = List.of(1, 2, 2, 3, 3, 3).stream()
        .distinct()
        .toList();   // [1, 2, 3]

// 对象去重:重写 equals,或先 map 出唯一键再 distinct
List<User> uniqueById = users.stream()
        .collect(Collectors.toMap(User::getId, u -> u, (a, b) -> a))
        .values().stream().toList();
// 或更简洁:
List<Long> uniqueIds = users.stream().map(User::getId).distinct().toList();

3.5 sorted():排序

java 复制代码
// 自然排序(元素需实现 Comparable)
List<String> sorted = names.stream().sorted().toList();

// 按年龄升序
List<User> byAge = users.stream()
        .sorted(Comparator.comparing(User::getAge))
        .toList();

// 多字段排序:年龄升序,年龄相同按名字降序
List<User> multi = users.stream()
        .sorted(Comparator.comparing(User::getAge)
                .thenComparing(User::getName, Comparator.reverseOrder()))
        .toList();

// null 安全的排序(name 可能为 null)
List<User> nullSafe = users.stream()
        .sorted(Comparator.comparing(User::getName, Comparator.nullsLast(String::compareTo)))
        .toList();

3.6 limit() 与 skip():截取与跳过

java 复制代码
// 分页场景:第 pageNum 页,每页 pageSize 条
List<User> page = users.stream()
        .skip((pageNum - 1) * pageSize)
        .limit(pageSize)
        .toList();

// 取年龄最大的前 3 名
List<User> top3 = users.stream()
        .sorted(Comparator.comparing(User::getAge).reversed())
        .limit(3)
        .toList();

3.7 peek():调试/旁路操作

java 复制代码
// peek 不改变流元素,常用于调试观察中间结果
List<String> result = users.stream()
        .peek(u -> log.debug("过滤前:{}", u))
        .filter(u -> u.getAge() > 18)
        .peek(u -> log.debug("过滤后:{}", u))
        .map(User::getName)
        .toList();

⚠️ peek 只适合调试。生产代码中不要用它做业务副作用,部分优化场景下(如 count())中间操作可能被跳过。

3.8 takeWhile() 与 dropWhile()(JDK 9)

filter 的本质区别:filter 检查所有 元素;takeWhile/dropWhile 遇到不满足条件的元素就停止判断(依赖流的顺序)。

java 复制代码
List<Integer> nums = List.of(1, 2, 3, 10, 1, 2);

// 从头取,直到遇到第一个不满足条件的元素为止
nums.stream().takeWhile(n -> n < 5).toList();    // [1, 2, 3]

// 从头丢弃,直到遇到第一个不满足条件的元素,之后全部保留
nums.stream().dropWhile(n -> n < 5).toList();    // [10, 1, 2]

3.9 mapToInt / mapToLong / mapToDouble:转原始类型流

java 复制代码
// 避免自动装箱,且能用上 sum/average 等数值专用操作
int totalAge = users.stream()
        .mapToInt(User::getAge)
        .sum();

double avgAge = users.stream()
        .mapToInt(User::getAge)
        .average()
        .orElse(0);

3.10 mapMulti()(JDK 16):flatMap 的高性能替代

mapMulti 用消费者回调代替返回流,避免了创建大量临时流对象的开销,适合"一个元素展开成 0~N 个元素"的场景。

java 复制代码
// 与 flatMap 等价:把每个订单的商品展开
List<Product> products = orders.stream()
        .<Product>mapMulti((order, consumer) -> {
            for (Product p : order.getItems()) {
                consumer.accept(p);
            }
        })
        .toList();

// 过滤 + 展开二合一:只保留金额大于 100 的商品
List<Product> expensive = orders.stream()
        .<Product>mapMulti((order, consumer) -> {
            for (Product p : order.getItems()) {
                if (p.getPrice().compareTo(BigDecimal.valueOf(100)) > 0) {
                    consumer.accept(p);
                }
            }
        })
        .toList();

四、终止操作

终止操作会触发整条流管道的实际执行,执行后流即关闭。

4.1 forEach() 与 forEachOrdered()

java 复制代码
users.stream().forEach(u -> System.out.println(u.getName()));

// 并行流中保证按原始顺序输出
users.parallelStream().forEachOrdered(System.out::println);

4.2 collect():收集为集合(配合 Collectors,见第五章)

java 复制代码
List<User> list = users.stream().collect(Collectors.toList());
Set<User>  set  = users.stream().collect(Collectors.toSet());

4.3 reduce():归约

java 复制代码
// 求和(带初始值)
int sum = nums.stream().reduce(0, Integer::sum);

// 求总金额(订单场景)
BigDecimal total = orders.stream()
        .map(Order::getAmount)
        .reduce(BigDecimal.ZERO, BigDecimal::add);

// 不带初始值 → 返回 Optional
Optional<Integer> max = nums.stream().reduce(Integer::max);

// 三参数版(并行流时用第三个参数合并各线程结果)
int parallelSum = nums.parallelStream().reduce(0, Integer::sum, Integer::sum);

4.4 count()

java 复制代码
long adultCount = users.stream().filter(u -> u.getAge() >= 18).count();

4.5 min() 与 max()

java 复制代码
Optional<User> oldest = users.stream()
        .max(Comparator.comparing(User::getAge));

Optional<Order> cheapest = orders.stream()
        .min(Comparator.comparing(Order::getAmount));

4.6 anyMatch / allMatch / noneMatch:短路匹配

java 复制代码
boolean hasAdmin  = users.stream().anyMatch(u -> u.getRole() == Role.ADMIN);   // 存在一个即返回
boolean allActive = users.stream().allMatch(u -> u.getStatus() == ACTIVE);     // 全部满足
boolean noneBanned = users.stream().noneMatch(User::isBanned);                 // 全都不满足

4.7 findFirst() 与 findAny():短路查找

java 复制代码
// findFirst:取第一个(顺序流中保证顺序)
Optional<User> first = users.stream()
        .filter(u -> u.getAge() > 18)
        .findFirst();

// findAny:取任意一个(并行流中性能更好)
Optional<User> any = users.parallelStream()
        .filter(u -> u.getAge() > 18)
        .findAny();

// 常见连招:找不到时抛业务异常
User user = users.stream()
        .filter(u -> u.getId().equals(id))
        .findFirst()
        .orElseThrow(() -> new BizException("用户不存在:" + id));

五、Collectors 收集器全解

collect(Collectors.xxx) 是 Stream 中功能最强大的部分,掌握它基本覆盖了日常 80% 的聚合需求。

5.1 收集为集合

java 复制代码
List<User> list     = users.stream().collect(Collectors.toList());
Set<User> set       = users.stream().collect(Collectors.toSet());

// JDK 10:不可变集合(推荐用于返回给外部的结果)
List<User> unmod    = users.stream().collect(Collectors.toUnmodifiableList());
Set<User> unmodSet  = users.stream().collect(Collectors.toUnmodifiableSet());

// JDK 16:最简写法,返回不可变 List(本文推荐写法)
List<User> jdk16    = users.stream().toList();

// 指定具体集合类型(如需要保持插入顺序的 LinkedHashSet、或需要 TreeSet)
TreeSet<User> treeSet = users.stream()
        .collect(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(User::getId))));

⚠️ 注意:stream().toList()(JDK 16)返回不可变 List,不能再 addCollectors.toList() 返回的是可变 ArrayList。需要后续修改结果集时要用后者。

5.2 joining():拼接字符串

java 复制代码
// "张三,李四,王五"
String names = users.stream()
        .map(User::getName)
        .collect(Collectors.joining(","));

// 带前缀后缀:"【张三|李四|王五】"
String pretty = users.stream()
        .map(User::getName)
        .collect(Collectors.joining("|", "【", "】"));

// 实战:拼接 IN 查询的 ID 列表
String idCsv = users.stream()
        .map(u -> String.valueOf(u.getId()))
        .collect(Collectors.joining(","));

5.3 toMap():收集为 Map

java 复制代码
// id → User(最常用:列表转 Map 方便按 key 查询)
Map<Long, User> userMap = users.stream()
        .collect(Collectors.toMap(User::getId, Function.identity()));

// key 冲突时必须给合并策略,否则抛 IllegalStateException
Map<String, User> byName = users.stream()
        .collect(Collectors.toMap(User::getName, Function.identity(), (old, neu) -> old));

// id → name(只取部分字段)
Map<Long, String> idNameMap = users.stream()
        .collect(Collectors.toMap(User::getId, User::getName));

// 指定 Map 实现(如 LinkedHashMap 保持顺序)
Map<Long, User> ordered = users.stream()
        .collect(Collectors.toMap(User::getId, Function.identity(), (a, b) -> a, LinkedHashMap::new));

5.4 groupingBy():分组(重点)

java 复制代码
// 按部门分组:Map<部门, List<员工>>
Map<String, List<User>> byDept = users.stream()
        .collect(Collectors.groupingBy(User::getDept));

// 分组后计数:Map<部门, 人数>
Map<String, Long> deptCount = users.stream()
        .collect(Collectors.groupingBy(User::getDept, Collectors.counting()));

// 分组后取字段集合:Map<部门, 姓名列表>
Map<String, List<String>> deptNames = users.stream()
        .collect(Collectors.groupingBy(User::getDept,
                Collectors.mapping(User::getName, Collectors.toList())));

// 分组后求和:Map<部门, 工资总额>
Map<String, Integer> deptSalary = users.stream()
        .collect(Collectors.groupingBy(User::getDept,
                Collectors.summingInt(User::getSalary)));

// 分组后求最大:Map<部门, 工资最高的员工>
Map<String, Optional<User>> topEarner = users.stream()
        .collect(Collectors.groupingBy(User::getDept,
                Collectors.maxBy(Comparator.comparing(User::getSalary))));

// 二级分组:先按部门,再按职级
Map<String, Map<String, List<User>>> nested = users.stream()
        .collect(Collectors.groupingBy(User::getDept,
                Collectors.groupingBy(User::getLevel)));

// 分组后转成 Set
Map<String, Set<User>> byDeptSet = users.stream()
        .collect(Collectors.groupingBy(User::getDept, Collectors.toSet()));

// 指定 Map 类型(如需要排序的 TreeMap)
TreeMap<String, List<User>> sorted = users.stream()
        .collect(Collectors.groupingBy(User::getDept, TreeMap::new, Collectors.toList()));

5.5 partitioningBy():二分(按布尔条件分两组)

java 复制代码
// true 组 / false 组,永远只有两个 key
Map<Boolean, List<User>> adultPartition = users.stream()
        .collect(Collectors.partitioningBy(u -> u.getAge() >= 18));

List<User> adults   = adultPartition.get(true);
List<User> minors   = adultPartition.get(false);

// 分区 + 下游收集器
Map<Boolean, Long> adultCount = users.stream()
        .collect(Collectors.partitioningBy(u -> u.getAge() >= 18, Collectors.counting()));

5.6 统计类:counting / summing / averaging / summarizing

java 复制代码
long count = users.stream().collect(Collectors.counting());

int totalSalary = users.stream().collect(Collectors.summingInt(User::getSalary));

double avgSalary = users.stream().collect(Collectors.averagingDouble(User::getSalary));

// 一次性拿到 数量/总和/最小/平均/最大
IntSummaryStatistics stats = users.stream()
        .collect(Collectors.summarizingInt(User::getSalary));
stats.getMax();
stats.getAverage();

5.7 maxBy() 与 minBy()

java 复制代码
Optional<User> oldest = users.stream()
        .collect(Collectors.maxBy(Comparator.comparing(User::getAge)));

5.8 reducing():通用归约收集器

java 复制代码
// 等价于 reduce,常用于作为下游收集器
int totalSalary = users.stream()
        .collect(Collectors.reducing(0, User::getSalary, Integer::sum));

5.9 collectingAndThen():收集后再加工

java 复制代码
// 分组后取每组最大值,并把 Optional 解包
Map<String, User> topByDept = users.stream()
        .collect(Collectors.groupingBy(User::getDept,
                Collectors.collectingAndThen(
                        Collectors.maxBy(Comparator.comparing(User::getSalary)),
                        Optional::get)));

// 收集为不可变集合
List<User> unmodifiable = users.stream()
        .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));

5.10 teeing()(JDK 12):一次流计算,两份结果

java 复制代码
// 同时求出平均工资和工资总额,一次遍历完成
record SalaryReport(double avg, int total) {}

SalaryReport report = users.stream()
        .collect(Collectors.teeing(
                Collectors.averagingDouble(User::getSalary),
                Collectors.summingInt(User::getSalary),
                SalaryReport::new));

六、原始类型流

处理大量数值时用 IntStream / LongStream / DoubleStream,避免装箱开销,并提供数值专用操作。

java 复制代码
IntStream intStream = users.stream().mapToInt(User::getAge);

int sum        = intStream.sum();                    // 求和
double avg     = users.stream().mapToInt(User::getAge).average().orElse(0);
int max        = users.stream().mapToInt(User::getAge).max().orElse(0);

// 统计摘要
IntSummaryStatistics stats = users.stream()
        .mapToInt(User::getAge)
        .summaryStatistics();

// 原始类型流 ↔ 对象流互转
Stream<Integer> boxed    = IntStream.range(1, 10).boxed();             // 装箱为 Stream<Integer>
IntStream backToPrimitive = boxed.mapToInt(Integer::intValue);         // 拆箱回 IntStream

// mapToObj:数值转对象
List<String> labels = IntStream.rangeClosed(1, 5)
        .mapToObj(i -> "第" + i + "名")
        .toList();

// 扁平化为原始类型流:统计所有订单的商品总数
long itemCount = orders.stream()
        .flatMapToLong(o -> LongStream.of(o.getItems().size()))
        .sum();

七、Stream 与 Optional 的配合

Optional 本身也提供了流式方法,配合使用可以优雅处理"可能为空"的值。

java 复制代码
Optional<User> opt = userRepository.findById(id);

// map:值存在则转换
String name = opt.map(User::getName).orElse("匿名");

// flatMap:转换函数本身也返回 Optional,避免 Optional 嵌套
Optional<String> email = opt.flatMap(u -> Optional.ofNullable(u.getEmail()));

// or(JDK 9):值为空时提供另一个 Optional
Optional<User> user = repo.findPrimary(id)
        .or(() -> repo.findBackup(id));

// stream(JDK 9):Optional → 0 或 1 个元素的流,适合批量场景
List<User> existing = ids.stream()
        .map(repo::findById)          // Stream<Optional<User>>
        .flatMap(Optional::stream)    // Stream<User>,空的被自然过滤
        .toList();

// ifPresentOrElse(JDK 9)
opt.ifPresentOrElse(
        u -> log.info("找到用户:{}", u.getName()),
        () -> log.warn("用户不存在"));

// 链式校验
User user = repo.findById(id)
        .filter(u -> !u.isBanned())
        .filter(u -> u.getAge() >= 18)
        .orElseThrow(() -> new BizException("用户不可用"));

八、并行流

java 复制代码
// 集合直接开并行
long count = users.parallelStream()
        .filter(u -> u.getAge() > 18)
        .count();

// 串行流中途切并行
long count2 = users.stream()
        .parallel()
        .filter(u -> u.getAge() > 18)
        .count();

// 并行时保证顺序输出
users.parallelStream().forEachOrdered(System.out::println);

什么时候可以用并行流

  • 数据量大(万级以上),且每个元素的处理是纯计算、无共享状态
  • 元素之间无依赖,操作满足结合律(如求和、统计);
  • 数据源易分割(ArrayList、数组;LinkedList、IO 流不适合)。

什么时候不要用

  • 数据量小(几百条以内),并行切换的开销比收益大;
  • 操作涉及数据库调用、RPC、文件 IO------并行流共用 ForkJoinPool.commonPool(),会阻塞公共池影响其他任务;
  • 有共享可变状态(如往外部集合 add),会有线程安全问题。

在 Spring Boot 的业务代码里,绝大多数场景都不该用并行流 。需要并行处理时优先考虑线程池 + CompletableFuture,可控性更强。


九、JDK 9~17 的 Stream 新特性汇总

Spring Boot 3 要求 JDK 17+,以下这些特性都可以放心使用:

版本 特性 示例
JDK 9 takeWhile / dropWhile stream.takeWhile(n -> n < 5)
JDK 9 iterate 带终止条件 Stream.iterate(1, n -> n < 100, n -> n * 2)
JDK 9 Stream.ofNullable Stream.ofNullable(maybeNull)
JDK 9 Optional.stream opt.stream().toList()
JDK 9 Optional.or / ifPresentOrElse opt.or(() -> other)
JDK 10 Collectors.toUnmodifiableList/Set/Map 收集为不可变集合
JDK 12 Collectors.teeing 一次遍历出两个结果
JDK 16 Stream.toList() 替代 collect(Collectors.toList())
JDK 16 Stream.mapMulti flatMap 的高性能替代
java 复制代码
// 新旧写法对比
List<String> oldWay = users.stream()
        .map(User::getName)
        .collect(Collectors.toList());        // JDK 8

List<String> newWay = users.stream()
        .map(User::getName)
        .toList();                             // JDK 16+,更短,且结果不可变

十、Spring Boot 项目实战场景

以下示例按 Spring Boot 3 + JDK 17 的典型分层结构编写,实体与 DTO 使用 record(JDK 17 正式特性)。

10.0 示例数据模型

java 复制代码
// 实体
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String dept;
    private BigDecimal salary;
    private UserStatus status;
    private LocalDateTime createTime;
    // getter/setter 省略
}

public enum UserStatus { ACTIVE, DISABLED, BANNED }

// 返回给前端的 VO(用 record,JDK 17)
public record UserVO(Long id, String name, Integer age, String deptName) {}

// 统计报表行
public record DeptStat(String dept, long count, double avgSalary, BigDecimal totalSalary) {}

10.1 场景一:列表查询 + DTO 转换(最高频)

java 复制代码
@Service
public class UserService {

    private final UserRepository userRepository;
    private final DeptService deptService;

    public List<UserVO> listActiveUsers() {
        return userRepository.findByStatus(UserStatus.ACTIVE).stream()
                .map(this::toVO)
                .toList();
    }

    private UserVO toVO(User user) {
        return new UserVO(user.getId(), user.getName(),
                user.getAge(), deptService.deptName(user.getDept()));
    }
}

10.2 场景二:批量查关联数据时避免循环单查(N+1 优化)

java 复制代码
public List<OrderVO> listOrderVOs(List<Order> orders) {
    // 1. 收集所有需要的用户 id(去重)
    Set<Long> userIds = orders.stream()
            .map(Order::getUserId)
            .collect(Collectors.toSet());

    // 2. 一次性批量查询,转成 Map
    Map<Long, User> userMap = userRepository.findByIds(userIds).stream()
            .collect(Collectors.toMap(User::getId, Function.identity()));

    // 3. 组装 VO,O(1) 查 Map
    return orders.stream()
            .map(o -> {
                User u = userMap.get(o.getUserId());
                return new OrderVO(o.getId(), o.getAmount(),
                        u == null ? "未知用户" : u.getName());
            })
            .toList();
}

10.3 场景三:分组统计报表

java 复制代码
public List<DeptStat> deptStatistics() {
    Map<String, List<User>> byDept = userRepository.findAll().stream()
            .filter(u -> u.getStatus() == UserStatus.ACTIVE)
            .collect(Collectors.groupingBy(User::getDept));

    return byDept.entrySet().stream()
            .map(e -> {
                List<User> members = e.getValue();
                BigDecimal total = members.stream()
                        .map(User::getSalary)
                        .reduce(BigDecimal.ZERO, BigDecimal::add);
                double avg = members.stream()
                        .mapToDouble(u -> u.getSalary().doubleValue())
                        .average().orElse(0);
                return new DeptStat(e.getKey(), members.size(), avg, total);
            })
            .sorted(Comparator.comparing(DeptStat::totalSalary).reversed())
            .toList();
}

10.4 场景四:多条件动态过滤

java 复制代码
public List<UserVO> search(String name, Integer minAge, String dept) {
    return userRepository.findAll().stream()
            // 条件为空则不过滤:把判空逻辑封装成谓词
            .filter(u -> name == null || u.getName().contains(name))
            .filter(u -> minAge == null || u.getAge() >= minAge)
            .filter(u -> dept == null || dept.equals(u.getDept()))
            .sorted(Comparator.comparing(User::getCreateTime).reversed())
            .map(this::toVO)
            .toList();
}

说明:数据量大时这种内存过滤应下推到数据库(用 Specification 或动态 SQL)。Stream 过滤适合数据量可控的场景(如配置列表、字典、已查出的结果集二次加工)。

10.5 场景五:列表转 Map 供快速查找

java 复制代码
// 部门编码 → 部门名称(字典缓存)
private Map<String, String> deptNameCache() {
    return deptRepository.findAll().stream()
            .collect(Collectors.toMap(Dept::getCode, Dept::getName, (a, b) -> a));
}

10.6 场景六:树形结构构建(菜单/部门树)

java 复制代码
public record MenuNode(Long id, Long parentId, String name, List<MenuNode> children) {}

public List<MenuNode> buildTree(List<Menu> menus) {
    // 先转成可变对象便于挂 children
    Map<Long, MenuNode> nodeMap = new HashMap<>();
    for (Menu m : menus) {
        nodeMap.put(m.getId(), new MenuNode(m.getId(), m.getParentId(), m.getName(), new ArrayList<>()));
    }
    // 二次遍历挂父子关系:Stream 负责筛选与分发
    menus.stream()
            .map(m -> nodeMap.get(m.getId()))
            .forEach(node -> {
                if (node.parentId() == null) {
                    return; // 根节点
                }
                MenuNode parent = nodeMap.get(node.parentId());
                if (parent != null) {
                    parent.children().add(node);
                }
            });
    return nodeMap.values().stream()
            .filter(n -> n.parentId() == null)
            .sorted(Comparator.comparing(MenuNode::id))
            .toList();
}

10.7 场景七:接口参数校验与预处理

java 复制代码
public void batchImport(List<String> rawLines) {
    List<UserImportDTO> validRows = rawLines.stream()
            .filter(StringUtils::hasText)              // 去空行
            .map(String::trim)                          // 去首尾空格
            .distinct()                                 // 去重
            .map(this::parseLine)                       // 解析(可能返回 null)
            .filter(Objects::nonNull)                   // 过滤解析失败的
            .toList();

    if (validRows.isEmpty()) {
        throw new BizException("没有可导入的有效数据");
    }
    userRepository.batchSave(validRows);
}

10.8 场景八:与 Optional 联动的单查

java 复制代码
public UserVO getUser(Long id) {
    return userRepository.findById(id)
            .filter(u -> u.getStatus() != UserStatus.BANNED)
            .map(this::toVO)
            .orElseThrow(() -> new BizException("用户不存在或已封禁:" + id));
}

十一、常见坑与最佳实践

11.1 流只能消费一次

java 复制代码
Stream<User> stream = users.stream().filter(u -> u.getAge() > 18);
stream.count();
stream.toList();   // ❌ 抛 IllegalStateException:stream has already been operated upon

做法 :每次重新从数据源创建流;若中间结果要复用,先 toList() 落地。

11.2 toList() 的结果不可变

java 复制代码
List<String> list = names.stream().toList();
list.add("x");     // ❌ UnsupportedOperationException(JDK 16 的 toList)

做法 :需要修改结果集时用 collect(Collectors.toList())

11.3 toMap 的 key 冲突

java 复制代码
users.stream().collect(Collectors.toMap(User::getDept, Function.identity()));
// ❌ 同一部门有多人时抛 IllegalStateException

做法 :永远给 toMap 加第三个参数(合并函数),除非你 100% 确定 key 唯一。

11.4 map 里返回 null 导致 NPE

java 复制代码
users.stream().map(u -> u.getAddress().getCity())   // ❌ address 为 null 直接 NPE

做法 :链路上先过滤,或用 Optional / 三元表达式兜底:

java 复制代码
users.stream()
     .filter(u -> u.getAddress() != null)
     .map(u -> u.getAddress().getCity())
     .toList();

11.5 forEach 里做业务副作用

java 复制代码
// ❌ 不推荐:隐藏副作用,并行化时会出线程安全问题
List<User> result = new ArrayList<>();
users.stream().forEach(u -> result.add(convert(u)));

// ✅ 正确:用 collect
List<User> result = users.stream().map(this::convert).toList();

11.6 在循环里反复建流

java 复制代码
// ❌ 每次循环都创建流,性能差且可读性低
for (User u : users) {
    if (orders.stream().anyMatch(o -> o.getUserId().equals(u.getId()))) { ... }
}

// ✅ 先把 orders 按 userId 建成 Map,O(1) 查找
Map<Long, List<Order>> ordersByUser = orders.stream()
        .collect(Collectors.groupingBy(Order::getUserId));
for (User u : users) {
    if (ordersByUser.containsKey(u.getId())) { ... }
}

11.7 为用而用

不是所有循环都该换成 Stream:

  • 简单遍历做副作用(如逐条调用外部接口),普通 for / forEach 更直白;
  • 逻辑复杂到 lambda 里塞了十几行,拆成方法或改回循环;
  • 需要 break/continue 精细控制流程时,循环更合适。

经验法则:Stream 适合"数据变换管道",循环适合"过程控制"。

11.8 调试技巧

链式调用报错时不好定位,可以:

  1. peek 打印中间结果;
  2. 把长链拆成几个有名字的中间变量(牺牲一点简洁换可读性);
  3. IDEA 中在 lambda 处打断点,Debug 模式可以直接看流内元素。

附:高频操作速查表

需求 写法
过滤 .filter(u -> u.getAge() > 18)
提取字段 .map(User::getName)
DTO 转换 .map(this::toVO).toList()
拍平嵌套 .flatMap(list -> list.stream())
去重 .distinct()toMap 保一
排序 .sorted(Comparator.comparing(User::getAge))
取前 N .limit(n)
跳过 N .skip(n)
求和 .mapToInt(...).sum()reduce
计数 .count()
最值 .max(comparator)
存在判断 .anyMatch(...)
取第一个 .findFirst().orElseThrow(...)
转 List .toList()(JDK 16+)
转 Map collect(Collectors.toMap(key, val, 合并函数))
分组 collect(Collectors.groupingBy(...))
拼接字符串 collect(Collectors.joining(","))
统计摘要 collect(Collectors.summarizingInt(...))
一次两个结果 collect(Collectors.teeing(a, b, 合并))

总结 :在 Spring Boot 3 + JDK 17 的项目里,Stream 已经是日常代码的"默认方言"------Service 层的列表转换用 map + toList,字典与缓存用 toMap,报表用 groupingBy,单查用 Optional 链。把本文第三~五章的操作当成工具箱、第十章的场景当成模板,再注意第十一章的几个坑,基本可以覆盖工作中 95% 的 Stream 需求。

相关推荐
不爱吃米饭_1 小时前
Apache PDFBox Java PDF处理库
java
mldong1 小时前
流程图上没画人,单子却走对了:工作流引擎内置处理人解析器
java·架构
tanzongbiao2 小时前
Dorado7文件上传 解决HttpServletResponse返回值中文乱码的问题
java·服务器·前端
茶本无香3 小时前
Arthas 实战:线上“看 Map”的四种武器
java·map·arthas·线上定位
霸道流氓气质4 小时前
Spring AI异常处理与重试机制
java·人工智能·spring
郑州光合科技余经理6 小时前
本地生活服务系统:成品模块和定制接口怎么划界
java·前端·人工智能·后端·系统架构·php·ai编程
萧瑟余晖6 小时前
Java深入解析篇三十三之密封类与接口(Sealed Classes)详解
java·开发语言
论迹复利10 小时前
FreeRTOS 在 RISC-V 上是如何“点火“的 —— 从 main() 到第一个任务的完整链路
java·开发语言·risc-v
张宇Joaquin10 小时前
鲲鹏统一并行加速库KUPL--众核并行能力介绍
java·开发语言·网络