在 Java 开发领域,数据处理是核心场景之一。传统的集合操作往往需要编写大量冗余的循环代码,不仅可读性差,还容易引入 bug。Java 8 引入的Stream API彻底改变了这一现状,它提供了一种声明式的数据处理方式,让开发者能够以更简洁、更优雅的代码实现复杂的数据筛选、转换、聚合等操作。

一、Stream API 核心概念:什么是 Stream?​

Stream(流)是 Java 8 中引入的一个抽象概念,它本质上是一系列支持聚合操作的元素序列,但与集合(Collection)有显著区别:​

  • 集合存储数据,Stream 处理数据:集合是数据的容器,关注 “数据是什么”;Stream 不存储数据,只关注 “如何处理数据”,且操作不会修改原始数据源。​
  • 惰性执行(Lazy Evaluation):Stream 的中间操作(如筛选、映射)不会立即执行,只有当触发终止操作(如收集、统计)时,才会一次性处理所有中间操作,减少无效计算。​
  • 一次性使用:一个 Stream 对象只能执行一次终止操作,再次使用会抛出IllegalStateException,需重新创建 Stream。​

Stream 的生命周期分为三个阶段:​

  1. 创建 Stream:从集合、数组、I/O 等数据源生成 Stream;​
  1. 中间操作(Intermediate Operations):对 Stream 进行筛选、映射、排序等处理,返回新的 Stream(可链式调用);​
  1. 终止操作(Terminal Operations):触发 Stream 的计算,返回非 Stream 类型的结果(如 List、Integer、void 等),结束 Stream 生命周期。

  

二、实战:Stream API 常用操作全解析​

下面通过 “用户数据处理” 的实战场景,详解 Stream 的核心操作。假设我们有一个User类,包含id、name、age、city、salary五个属性,且有一个用户列表List<User> userList作为数据源。​

1. 1. 创建 Stream:3 种常用方式

// 1. 从集合创建(最常用)
Stream<User> streamFromList = userList.stream(); // 串行流
Stream<User> parallelStream = userList.parallelStream(); // 并行流(多线程处理)

// 2. 从数组创建
User[] userArray = userList.toArray(new User[0]);
Stream<User> streamFromArray = Arrays.stream(userArray);

// 3. 从静态方法创建(空流、单个元素、多个元素)
Stream<User> emptyStream = Stream.empty();
Stream<User> singleElementStream = Stream.of(new User(1, "Alice", 25, "Beijing", 8000));
Stream<User> multiElementStream = Stream.of(new User(1, "Alice", 25, "Beijing", 8000), 
                                            new User(2, "Bob", 30, "Shanghai", 12000));

2. 中间操作:筛选、映射与排序​

中间操作支持链式调用,常见操作如下:​

(1)筛选(Filter):按条件保留元素​

需求:筛选出 “年龄大于 25 岁且在上海工作” 的用户

Stream<User> filteredStream = userList.stream()
                                      .filter(user -> user.getAge() > 25) // 年龄>25
                                      .filter(user -> "Shanghai".equals(user.getCity())); // 城市=上海

2)映射(Map/FlatMap):转换元素类型​

  • Map:将 Stream 中的每个元素转换为另一种类型(1:1 映射);​
  • FlatMap:将 Stream 中的每个元素转换为一个 Stream,再合并为一个 Stream(1:N 映射,解决 “流嵌套” 问题)。​

需求 1:提取筛选后用户的 “姓名和薪资”,转换为Map<String, Integer>(姓名→薪资)

Map<String, Integer> nameToSalaryMap = userList.stream()
                                               .filter(user -> user.getAge() > 25)
                                               .collect(Collectors.toMap(
                                                   User::getName, // key:姓名
                                                   User::getSalary, // value:薪资
                                                   (v1, v2) -> v1 // 解决key重复:保留第一个值
                                               ));

需求 2:提取所有用户的 “城市”,并去重(FlatMap 示例)

// 假设User有一个方法getTags(),返回List<String>(如["技术岗", "全职"]),需提取所有不重复的标签
Set<String> allTags = userList.stream()
                              .flatMap(user -> user.getTags().stream()) // 将每个用户的标签列表转为Stream,再合并
                              .collect(Collectors.toSet()); // 去重并收集为Set

(3)排序(Sorted):按指定规则排序​

需求:按 “薪资降序” 排序,薪资相同则按 “年龄升序” 排序

Stream<User> sortedStream = userList.stream()
                                    .sorted(
                                        // 先按薪资降序,再按年龄升序
                                        Comparator.comparingInt(User::getSalary).reversed()
                                                  .thenComparingInt(User::getAge)
                                    );

   

3. 终止操作:收集、统计与匹配​

终止操作触发 Stream 计算,常见操作如下:​

(1)收集(Collect):将 Stream 转换为集合或其他结构​

Collectors工具类提供了丰富的收集器,满足绝大多数场景:

// 1. 收集为List(去重)
List<User> filteredUserList = userList.stream()
                                      .filter(user -> user.getSalary() > 10000)
                                      .distinct() // 去重(基于equals()方法)
                                      .collect(Collectors.toList());

// 2. 收集为Set(自动去重)
Set<String> citySet = userList.stream()
                              .map(User::getCity)
                              .collect(Collectors.toSet());

// 3. 分组(GroupingBy):按城市分组,统计每个城市的用户数量
Map<String, Long> cityUserCount = userList.stream()
                                          .collect(Collectors.groupingBy(
                                              User::getCity, // 分组key:城市
                                              Collectors.counting() // 分组value:用户数量
                                          ));

// 4. 分区(PartitioningBy):按布尔条件分为两组(true组和false组)
// 需求:将用户分为“薪资>10000”和“薪资≤10000”两组
Map<Boolean, List<User>> salaryPartition = userList.stream()
                                                   .collect(Collectors.partitioningBy(
                                                       user -> user.getSalary() > 10000
                                                   ));

(2)统计(SummaryStatistics):数值型数据统计​

需求:统计所有用户的 “薪资” 相关指标(总和、平均值、最大值、最小值、数量)

IntSummaryStatistics salaryStats = userList.stream()
                                           .mapToInt(User::getSalary) // 转换为IntStream
                                           .summaryStatistics();

System.out.println("薪资总和:" + salaryStats.getSum());
System.out.println("薪资平均值:" + salaryStats.getAverage());
System.out.println("最高薪资:" + salaryStats.getMax());
System.out.println("最低薪资:" + salaryStats.getMin());
System.out.println("用户数量:" + salaryStats.getCount());

(3)匹配(AnyMatch/AllMatch/NoneMatch):判断是否满足条件​

  • anyMatch:是否存在至少一个元素满足条件;​
  • allMatch:是否所有元素都满足条件;​
  • noneMatch:是否所有元素都不满足条件。
  • // 1. 是否存在“北京的用户”
    boolean hasBeijingUser = userList.stream()
                                     .anyMatch(user -> "Beijing".equals(user.getCity()));
    
    // 2. 是否所有用户的年龄都大于18岁
    boolean allAdult = userList.stream()
                               .allMatch(user -> user.getAge() > 18);
    
    // 3. 是否没有“薪资低于5000”的用户
    boolean noLowSalary = userList.stream()
                                  .noneMatch(user -> user.getSalary() < 5000);

三、性能优化:避免 Stream 使用中的 “坑”​

Stream 虽然简洁,但如果使用不当,可能导致性能问题。以下是 3 个关键优化点:​

1. 优先使用串行流,避免滥用并行流​

并行流(parallelStream)通过多线程处理数据,看似更快,但存在线程开销和线程安全问题:​

  • 当数据量较小时(如小于 1000 条),串行流的性能反而更好(避免线程创建 / 切换开销);​
  • 并行流使用的是ForkJoinPool.commonPool(),若线程池被耗尽,可能导致其他任务阻塞;​
  • 并行流处理过程中,若操作涉及非线程安全的对象(如ArrayList的add方法),会导致数据错乱。​

优化建议:​

  • 数据量小(<1000):用串行流(stream());​
  • 数据量大且操作复杂(如大量计算):用并行流,并指定自定义线程池(避免占用公共池):
  • // 自定义线程池,避免占用公共池
    ForkJoinPool customPool = new ForkJoinPool(4); // 4个核心线程
    try {
        List<User> result = customPool.submit(() -> 
            userList.parallelStream()
                    .filter(user -> user.getSalary() > 10000)
                    .collect(Collectors.toList())
        ).get();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        customPool.shutdown(); // 关闭线程池
    }

    2. 避免重复创建 Stream​

    Stream 是一次性使用的,但有些开发者会在循环中重复创建 Stream,导致不必要的开销:

  • // 错误:循环中重复创建Stream
    for (String city : cityList) {
        long count = userList.stream()
                             .filter(user -> city.equals(user.getCity()))
                             .count();
    }
    
    // 优化:先分组,再查询(只创建一次Stream)
    Map<String, Long> cityCountMap = userList.stream()
                                             .collect(Collectors.groupingBy(User::getCity, Collectors.counting()));
    for (String city : cityList) {
        long count = cityCountMap.getOrDefault(city, 0L); // 直接从Map查询,无需重复流操作
    }

    3. 中间操作的顺序影响性能​

    中间操作的顺序会影响 Stream 的处理效率,筛选操作(如 filter)应放在前面,减少后续操作的元素数量:

  • // 低效:先排序(O(n log n)),再筛选(减少元素)
    Stream<User> inefficientStream = userList.stream()
                                              .sorted(Comparator.comparingInt(User::getSalary))
                                              .filter(user -> user.getAge() > 25);
    
    // 高效:先筛选(减少元素),再排序(处理的元素更少,排序更快)
    Stream<User> efficientStream = userList.stream()
                                            .filter(user -> user.getAge() > 25)
                                            .sorted(Comparator.comparingInt(User::getSalary));

    四、总结:Stream API 的核心价值​

    Stream API 并非替代集合,而是为集合提供了更高效的 “数据处理工具”。其核心价值在于:​

  • 代码更简洁:用声明式语法替代嵌套循环,减少冗余代码,提升可读性;​
  • 逻辑更清晰:链式调用将 “筛选→映射→排序→收集” 的流程直观展现,便于维护;​
  • 易于并行化:只需将stream()改为parallelStream()(或自定义线程池),即可实现多线程处理,无需手动编写线程代码。​
  • 掌握 Stream API 是 Java 开发者从 “基础开发” 迈向 “高效开发” 的关键一步。建议在实际项目中多尝试用 Stream 替代传统循环,结合本文的优化技巧,让代码既优雅又高效。

更多推荐