文章 585
评论 5
浏览 226854
5 个你肯定用得到的 Stream API 骚操作

5 个你肯定用得到的 Stream API 骚操作

一、引言

Java Stream API 已经成为后端开发的标配,但很多开发者只停留在 filtermapcollect 这些基础用法上。

今天我要分享 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 详细对比

APIJava 版本功能描述常见陷阱
Collectors.teeing()12+同时进行两种聚合操作需要 Java 12+,旧环境不支持
Stream.iterate(seed, hasNext, next)9+生成带终止条件的流hasNext 是终止条件,不是过滤条件
groupingBy + mapping8+分组 + 下游转换多层嵌套时类型推断可能失败
takeWhile/dropWhile9+有序流的条件截断在无序流上结果不确定
flatMap + Optional::stream9+优雅过滤 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 核心原则

  1. 语义清晰:选择最能表达业务意图的 API
  2. 性能优先:短路操作(takeWhile/dropWhile)比 filter 更高效
  3. 版本兼容:注意目标环境的 Java 版本限制
  4. 组合使用:多个 API 配合使用效果更好

9.3 进阶学习建议

进阶学习路径:
1. 深入理解 Collector 的组合机制
2. 学习自定义 Collector 的实现
3. 掌握 Stream 的并行处理(parallelStream)
4. 了解 Reactor 等响应式编程库(流式 API 的超集)

💡 互动话题:你在项目中常用哪些 Stream API 的进阶用法?有没有遇到过什么坑?欢迎在评论区分享你的经验!


标题:5 个你肯定用得到的 Stream API 骚操作
作者:jiangyi
地址:http://jiangyi.space/articles/2026/07/17/1783850078886.html
公众号:服务端技术精选

服务端开发博客:后端架构、高并发、性能优化与微服务实战教程

取消