一、引言
Java Stream API 已经成为后端开发的标配,但很多开发者只停留在 filter、map、collect 这些基础用法上。
今天我要分享 5 个进阶的 Stream API 操作,每一个都能让你的代码更简洁、更优雅,帮你写出"一行代码搞定"的操作。
二、操作一:Collectors.teeing() — 同时做两种聚合
2.1 功能介绍
Collectors.teeing() 可以在一次流处理中同时进行两种不同的聚合操作,然后将结果合并。
Java 版本要求:Java 12+
2.2 传统写法
// 需求:同时计算订单总金额和平均金额
List<Order> orders = getOrders();
// 第一种方式:遍历两次
double total = orders.stream()
.mapToDouble(Order::getAmount)
.sum();
double average = orders.stream()
.mapToDouble(Order::getAmount)
.average()
.orElse(0);
System.out.println("总金额: " + total + ", 平均金额: " + average);
// 第二种方式:手动计算
double total2 = 0;
int count = 0;
for (Order order : orders) {
total2 += order.getAmount();
count++;
}
double average2 = count > 0 ? total2 / count : 0;
2.3 使用 teeing() 简化
// 使用 teeing() 一行搞定
var result = orders.stream()
.collect(Collectors.teeing(
Collectors.summingDouble(Order::getAmount), // 聚合器1:求和
Collectors.averagingDouble(Order::getAmount), // 聚合器2:求平均
(total, avg) -> Map.of("total", total, "average", avg) // 合并函数
));
System.out.println("总金额: " + result.get("total") + ", 平均金额: " + result.get("average"));
2.4 自定义结果类
record Statistics(double total, double average, long count) {}
// 方式一:使用两次 teeing 嵌套
Statistics stats = orders.stream()
.collect(Collectors.teeing(
Collectors.summingDouble(Order::getAmount),
Collectors.counting(),
(total, count) -> {
double avg = count > 0 ? total / count : 0;
return new Statistics(total, avg, count);
}
));
// 方式二:使用 averagingDouble 直接计算平均值
Statistics stats2 = orders.stream()
.collect(Collectors.teeing(
Collectors.summingDouble(Order::getAmount),
Collectors.teeing(
Collectors.averagingDouble(Order::getAmount),
Collectors.counting(),
(avg, count) -> new Object() {
final double average = avg;
final long countValue = count;
}
),
(total, avgAndCount) -> new Statistics(total, avgAndCount.average, avgAndCount.countValue)
));
2.5 适用场景
| 场景 | 聚合器1 | 聚合器2 |
|---|---|---|
| 统计分析 | 求和 | 平均值 |
| 数据校验 | 最大值 | 最小值 |
| 报表生成 | 总数 | 符合条件数 |
| 性能监控 | 总耗时 | 最大耗时 |
三、操作二:Stream.iterate() 带终止条件 — 生成有限流
3.1 功能介绍
Stream.iterate() 的三参数重载版本可以生成带有终止条件的流,避免无限流。
Java 版本要求:Java 9+
3.2 方法签名
// 三参数版本:seed 起始值, hasNext 终止条件, next 生成下一个值
static <T> Stream<T> iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next)
// 注意:hasNext 是"继续条件",返回 false 时停止
// 不是"过滤条件"!这是最容易混淆的地方
3.3 传统写法
// 需求:生成 1 到 10 的平方数
// 方式一:for 循环
List<Integer> squares = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
squares.add(i * i);
}
// 方式二:limit 限制
List<Integer> squares2 = Stream.iterate(1, i -> i + 1)
.limit(10)
.map(i -> i * i)
.collect(Collectors.toList());
3.4 使用带终止条件的 iterate()
// 三参数版本:更清晰表达终止条件
List<Integer> squares = Stream.iterate(
1, // 起始值
i -> i <= 10, // 终止条件:当 i > 10 时停止
i -> i + 1 // 生成下一个值
)
.map(i -> i * i)
.collect(Collectors.toList());
// 结果:[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
3.5 复杂示例:生成斐波那契数列
// 生成斐波那契数列,直到超过 1000
record Fibonacci(int prev, int current) {}
List<Integer> fibonacci = Stream.iterate(
new Fibonacci(0, 1),
f -> f.current() <= 1000,
f -> new Fibonacci(f.current(), f.prev() + f.current())
)
.map(Fibonacci::current)
.collect(Collectors.toList());
// 结果:[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]
3.6 ⚠️ 常见陷阱
// ❌ 错误:将 hasNext 当作过滤条件
Stream.iterate(1, i -> i % 2 == 0, i -> i + 1)
.forEach(System.out::println); // 输出:1(因为 1 % 2 != 0,直接停止)
// ✅ 正确:先用 filter 过滤,再用 iterate 生成
Stream.iterate(2, i -> i <= 20, i -> i + 2)
.forEach(System.out::println); // 输出:2, 4, 6, 8, 10, 12, 14, 16, 18, 20
四、操作三:Collectors.groupingBy + mapping — 多层分组转换
4.1 功能介绍
Collectors.groupingBy() 可以结合 Collectors.mapping() 实现多层分组和下游转换,一行代码替代嵌套循环。
Java 版本要求:Java 8+
4.2 传统写法
// 需求:按部门分组,获取每个部门的员工姓名集合
List<Employee> employees = getEmployees();
// 方式一:嵌套循环
Map<String, Set<String>> deptNames = new HashMap<>();
for (Employee emp : employees) {
String dept = emp.getDepartment();
deptNames.computeIfAbsent(dept, k -> new HashSet<>())
.add(emp.getName());
}
// 方式二:先分组再转换(两次遍历)
Map<String, List<Employee>> deptEmployees = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
Map<String, Set<String>> deptNames2 = new HashMap<>();
for (Map.Entry<String, List<Employee>> entry : deptEmployees.entrySet()) {
deptNames2.put(entry.getKey(),
entry.getValue().stream()
.map(Employee::getName)
.collect(Collectors.toSet()));
}
4.3 使用 groupingBy + mapping 简化
// 一行代码搞定:分组 + 下游转换
Map<String, Set<String>> deptNames = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment, // 分组键
Collectors.mapping( // 下游收集器
Employee::getName, // 转换函数
Collectors.toSet() // 最终收集器
)
));
// 结果:{
// "研发部": ["张三", "李四", "王五"],
// "产品部": ["赵六", "钱七"],
// "运维部": ["孙八"]
// }
4.4 多层分组示例
// 需求:按部门分组,再按职级分组,最后获取员工姓名列表
Map<String, Map<String, List<String>>> result = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getLevel,
Collectors.mapping(
Employee::getName,
Collectors.toList()
)
)
));
// 结果:{
// "研发部": {
// "高级工程师": ["张三"],
// "工程师": ["李四", "王五"]
// },
// "产品部": {
// "产品经理": ["赵六"],
// "产品助理": ["钱七"]
// }
// }
4.5 分组 + 统计
// 需求:按部门分组,统计每个部门的平均薪资
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
五、操作四:takeWhile/dropWhile — 有序流的条件截断
5.1 功能介绍
takeWhile() 和 dropWhile() 是短路操作,在遇到第一个不匹配的元素时立即停止处理。
Java 版本要求:Java 9+
5.2 方法对比
| 方法 | 行为 | 示例 |
|---|---|---|
takeWhile(predicate) | 保留满足条件的前缀,遇到第一个不满足条件的元素停止 | [1,2,3,4,5].takeWhile(i < 4) → [1,2,3] |
dropWhile(predicate) | 丢弃满足条件的前缀,遇到第一个不满足条件的元素开始保留剩余元素 | [1,2,3,4,5].dropWhile(i < 4) → [4,5] |
5.3 与 filter 的区别
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 4, 3, 2, 1);
// filter:遍历所有元素
List<Integer> filtered = numbers.stream()
.filter(i -> i < 4)
.collect(Collectors.toList());
// 结果:[1, 2, 3, 3, 2, 1]
// takeWhile:短路操作,遇到第一个不满足条件的元素停止
List<Integer> taken = numbers.stream()
.takeWhile(i -> i < 4)
.collect(Collectors.toList());
// 结果:[1, 2, 3]
// dropWhile:短路操作,丢弃满足条件的前缀
List<Integer> dropped = numbers.stream()
.dropWhile(i -> i < 4)
.collect(Collectors.toList());
// 结果:[4, 5, 4, 3, 2, 1]
5.4 实战场景:处理分页数据
// 需求:从日志中提取连续的错误日志,直到遇到第一个非错误日志
List<String> logs = List.of(
"ERROR: 数据库连接失败",
"ERROR: 重试连接...",
"ERROR: 连接超时",
"INFO: 服务启动成功",
"ERROR: 另一个错误" // 不会被提取
);
List<String> errorLogs = logs.stream()
.takeWhile(log -> log.startsWith("ERROR"))
.collect(Collectors.toList());
// 结果:["ERROR: 数据库连接失败", "ERROR: 重试连接...", "ERROR: 连接超时"]
5.5 ⚠️ 无序流警告
// 在无序流上使用 takeWhile/dropWhile,结果不确定!
Set<Integer> unorderedSet = Set.of(3, 1, 4, 2, 5);
// 结果可能是任意子集,取决于内部迭代顺序
unorderedSet.stream()
.takeWhile(i -> i < 4)
.forEach(System.out::println);
// ✅ 建议:先排序或使用有序集合
unorderedSet.stream()
.sorted()
.takeWhile(i -> i < 4)
.forEach(System.out::println); // 1, 2, 3
六、操作五:flatMap + Optional::stream — 优雅处理 Optional
6.1 功能介绍
Optional.stream() 将 Optional 转换为包含 0 或 1 个元素的 Stream,配合 flatMap 可以优雅地过滤空值。
Java 版本要求:Java 9+
6.2 传统写法
// 需求:从用户列表中提取所有用户的邮箱(过滤掉空邮箱)
List<User> users = getUsers();
// 方式一:filter + map(需要两次操作)
List<String> emails = users.stream()
.map(User::getEmail) // 获取 Optional<String>
.filter(Optional::isPresent) // 过滤空值
.map(Optional::get) // 提取值
.collect(Collectors.toList());
// 方式二:手动处理
List<String> emails2 = new ArrayList<>();
for (User user : users) {
Optional<String> email = user.getEmail();
if (email.isPresent()) {
emails2.add(email.get());
}
}
6.3 使用 flatMap + Optional::stream 简化
// 一行代码:flatMap + Optional::stream
List<String> emails = users.stream()
.flatMap(user -> user.getEmail().stream())
.collect(Collectors.toList());
6.4 原理解析
// Optional.stream() 的行为
Optional<String> emptyOpt = Optional.empty();
Optional<String> presentOpt = Optional.of("test@example.com");
// 空 Optional → 空 Stream
emptyOpt.stream().count(); // 0
// 非空 Optional → 包含一个元素的 Stream
presentOpt.stream().count(); // 1
// flatMap 会将多个 Stream 合并为一个
// 空 Stream 自然被过滤掉
6.5 复杂示例:嵌套 Optional 处理
// 需求:获取订单的商品分类(可能为空)
record Order(Optional<Product> product) {}
record Product(Optional<Category> category) {}
record Category(String name) {}
List<Order> orders = getOrders();
// 使用 flatMap 链式处理嵌套 Optional
List<String> categories = orders.stream()
.flatMap(order -> order.product().stream())
.flatMap(product -> product.category().stream())
.map(Category::name)
.collect(Collectors.toList());
6.6 对比其他方式
// ❌ 方式一:嵌套 ifPresent(代码冗长)
List<String> categories1 = new ArrayList<>();
for (Order order : orders) {
order.product().ifPresent(product -> {
product.category().ifPresent(category -> {
categories1.add(category.name());
});
});
}
// ❌ 方式二:map + filter + map(可读性差)
List<String> categories2 = orders.stream()
.map(Order::product)
.filter(Optional::isPresent)
.map(Optional::get)
.map(Product::category)
.filter(Optional::isPresent)
.map(Optional::get)
.map(Category::name)
.collect(Collectors.toList());
// ✅ 方式三:flatMap + Optional::stream(优雅)
List<String> categories3 = orders.stream()
.flatMap(o -> o.product().stream())
.flatMap(p -> p.category().stream())
.map(Category::name)
.collect(Collectors.toList());
七、组合使用示例
7.1 示例一:生成序列并统计
// 生成 1 到 100 的偶数,同时计算总和和平均值
var stats = Stream.iterate(2, i -> i <= 100, i -> i + 2)
.collect(Collectors.teeing(
Collectors.summingInt(Integer::intValue),
Collectors.averagingInt(Integer::intValue),
(sum, avg) -> Map.of("sum", sum, "avg", avg)
));
// 结果:sum = 2550, avg = 51.0
7.2 示例二:处理分页 API 响应
// 模拟分页 API 响应,获取所有数据直到没有更多
record PageResponse(List<Item> items, boolean hasMore) {}
// 使用 AtomicInteger 追踪页码
AtomicInteger pageNumber = new AtomicInteger(1);
List<Item> allItems = Stream.iterate(
fetchPage(pageNumber.get()), // 起始页
PageResponse::hasMore, // 终止条件:没有更多数据时停止
page -> fetchPage(pageNumber.incrementAndGet()) // 获取下一页
)
.flatMap(page -> page.items().stream())
.collect(Collectors.toList());
7.3 示例三:分组统计非空数据
// 按部门分组,统计每个部门有邮箱的员工比例
Map<String, Double> emailRateByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.teeing(
Collectors.counting(),
Collectors.mapping(
Employee::getEmail,
Collectors.filtering(Optional::isPresent, Collectors.counting())
),
(total, hasEmail) -> (double) hasEmail / total
)
));
八、API 对比总结
8.1 详细对比
| API | Java 版本 | 功能描述 | 常见陷阱 |
|---|---|---|---|
| Collectors.teeing() | 12+ | 同时进行两种聚合操作 | 需要 Java 12+,旧环境不支持 |
| Stream.iterate(seed, hasNext, next) | 9+ | 生成带终止条件的流 | hasNext 是终止条件,不是过滤条件 |
| groupingBy + mapping | 8+ | 分组 + 下游转换 | 多层嵌套时类型推断可能失败 |
| takeWhile/dropWhile | 9+ | 有序流的条件截断 | 在无序流上结果不确定 |
| flatMap + Optional::stream | 9+ | 优雅过滤 Optional | 需要理解 flatMap 的合并机制 |
8.2 使用建议
使用决策树:
┌─────────────────────────────────────────────────────┐
│ 需要同时计算多个聚合值? │
│ └─ Collectors.teeing()(注意 Java 版本) │
│ │
│ 需要生成有终止条件的序列? │
│ └─ Stream.iterate(seed, hasNext, next) │
│ │
│ 需要分组并转换结果? │
│ └─ groupingBy + mapping │
│ │
│ 需要按条件截断有序流? │
│ └─ takeWhile/dropWhile │
│ │
│ 需要过滤 Optional 中的值? │
│ └─ flatMap + Optional::stream │
└─────────────────────────────────────────────────────┘
8.3 Java 版本兼容性
Java 版本支持情况:
┌─────────────────────────────────────────────────────┐
│ Java 8: │
│ ✓ groupingBy + mapping │
│ │
│ Java 9+: │
│ ✓ Stream.iterate(seed, hasNext, next) │
│ ✓ takeWhile / dropWhile │
│ ✓ Optional::stream │
│ │
│ Java 12+: │
│ ✓ Collectors.teeing() │
└─────────────────────────────────────────────────────┘
九、总结
9.1 五个骚操作回顾
| 操作 | 核心价值 | 代码示例 |
|---|---|---|
| teeing() | 一次遍历,多次聚合 | .collect(teeing(sum, avg, merge)) |
| iterate() | 生成有限流,语义清晰 | iterate(1, i <= 10, i -> i + 1) |
| groupingBy + mapping | 一行搞定分组转换 | groupingBy(key, mapping(value, toSet())) |
| takeWhile/dropWhile | 短路操作,性能优化 | .takeWhile(i -> i < 4) |
| flatMap + Optional::stream | 优雅处理空值 | .flatMap(opt -> opt.stream()) |
9.2 核心原则
- 语义清晰:选择最能表达业务意图的 API
- 性能优先:短路操作(takeWhile/dropWhile)比 filter 更高效
- 版本兼容:注意目标环境的 Java 版本限制
- 组合使用:多个 API 配合使用效果更好
9.3 进阶学习建议
进阶学习路径:
1. 深入理解 Collector 的组合机制
2. 学习自定义 Collector 的实现
3. 掌握 Stream 的并行处理(parallelStream)
4. 了解 Reactor 等响应式编程库(流式 API 的超集)
💡 互动话题:你在项目中常用哪些 Stream API 的进阶用法?有没有遇到过什么坑?欢迎在评论区分享你的经验!
