彻底掌握Java Stream:覆盖日常开发90%场景附代码
2025-06-24 08:24 阅读(34)

吃透JAVA的Stream流操作:多年实践总结

前言

当看到同事用几行Stream优雅实现你几十行的分组统计代码时;

当需求变更需要新增过滤条件,你不得不重构整个循环逻辑时;

当面对百万级数据集合,传统遍历性能捉襟见肘时...

一、Stream核心概念

什么是Stream?

Stream不是数据结构,而是对数据源(集合、数组等)进行高效聚合操作(filter、map、reduce等)的计算工具

Stream操作特点:


惰性执行:中间操作不会立即执行,直到遇到终端操作

不可复用:Stream只能被消费一次

不修改源数据:所有操作返回新Stream

支持并行处理:parallelStream()开启并行处理


操作类型:

操作类型方法示例说明
创建流stream(), parallelStream()创建流对象
中间操作filter(), map(), sorted()返回新Stream
终端操作collect(), forEach(), reduce()触发计算并关闭流


二、常见业务场景与实现

场景1:数据筛选与转换

需求:从用户列表中筛选VIP用户并提取联系信息

List<User> users = Arrays.asList(
    new User(1, "张三", "zhangsan@example.com", "13800138000", true),
    new User(2, "李四", "lisi@example.com", "13900139000", false),
    new User(3, "王五", "wangwu@example.com", "13700137000", true)
);

// 传统方式
List<UserContact> vipContacts = new ArrayList<>();
for (User user : users) {
    if (user.isVip()) {
        vipContacts.add(new UserContact(
            user.getName(), 
            user.getEmail(), 
            user.getPhone()
        ));
    }
}

// Stream方式
List<UserContact> streamContacts = users.stream()
    .filter(User::isVip) // 过滤VIP用户(方法引用)
    .map(user -> new UserContact( // 转换为Contact对象
        user.getName(), 
        user.getEmail(), 
        user.getPhone()
    ))
    .collect(Collectors.toList()); // 收集为List

System.out.println("VIP联系人:");
streamContacts.forEach(System.out::println);


场景2:数据分组统计

需求:按部门统计员工数量和平均薪资

List<Employee> employees = Arrays.asList(
    new Employee("张三", "研发部", 15000),
    new Employee("李四", "市场部", 12000),
    new Employee("王五", "研发部", 18000),
    new Employee("赵六", "人事部", 10000),
    new Employee("钱七", "市场部", 14000)
);

// 传统方式
Map<String, List<Employee>> deptMap = new HashMap<>();
for (Employee emp : employees) {
    deptMap.computeIfAbsent(emp.getDepartment(), k -> new ArrayList<>()).add(emp);
}

Map<String, Double> avgSalary = new HashMap<>();
for (Map.Entry<String, List<Employee>> entry : deptMap.entrySet()) {
    double sum = 0;
    for (Employee emp : entry.getValue()) {
        sum += emp.getSalary();
    }
    avgSalary.put(entry.getKey(), sum / entry.getValue().size());
}

// Stream方式
Map<String, Long> countByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment, 
        Collectors.counting()
    ));

Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)
    ));

System.out.println("部门人数统计: " + countByDept);
System.out.println("部门平均薪资: " + avgSalaryByDept);


场景3:多级分组与统计

需求:按部门分组,再按薪资范围分组统计

// 定义薪资范围函数
Function<Employee, String> salaryLevel = emp -> {
    if (emp.getSalary() < 10000) return "初级";
    else if (emp.getSalary() < 15000) return "中级";
    else return "高级";
};

// 多级分组
Map<String, Map<String, Long>> deptSalaryLevelCount = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.groupingBy(
            salaryLevel,
            Collectors.counting()
        )
    ));

System.out.println("部门薪资级别统计:");
deptSalaryLevelCount.forEach((dept, levelMap) -> {
    System.out.println("[" + dept + "部门]");
    levelMap.forEach((level, count) -> 
        System.out.println("  " + level + "级: " + count + "人")
    );
});


场景4:数据排序与分页

需求:按薪资降序排序,实现分页查询

int pageSize = 2;
int pageNum = 1; // 第一页

List<Employee> pageResult = employees.stream()
    .sorted(Comparator.comparingDouble(Employee::getSalary).reversed()) // 薪资降序
    .skip((pageNum - 1) * pageSize) // 跳过前面的记录
    .limit(pageSize) // 限制每页数量
    .collect(Collectors.toList());

System.out.println("\n分页结果(第" + pageNum + "页):");
pageResult.forEach(emp -> 
    System.out.println(emp.getName() + ": " + emp.getSalary())
);


场景5:数据匹配与查找

需求:检查是否存在高薪员工,查找第一个研发部员工

// 检查是否存在薪资>20000的员工
boolean hasHighSalary = employees.stream()
    .anyMatch(emp -> emp.getSalary() > 20000);

// 查找第一个研发部员工
Optional<Employee> firstDev = employees.stream()
    .filter(emp -> "研发部".equals(emp.getDepartment()))
    .findFirst();

System.out.println("\n是否存在高薪员工: " + hasHighSalary);
firstDev.ifPresent(emp -> 
    System.out.println("第一个研发部员工: " + emp.getName())
);


场景6:数据归约与聚合

需求:计算公司总薪资支出和最高薪资

// 计算总薪资
double totalSalary = employees.stream()
    .mapToDouble(Employee::getSalary)
    .sum();

// 使用reduce计算最高薪资
Optional<Employee> maxSalaryEmployee = employees.stream()
    .reduce((e1, e2) -> e1.getSalary() > e2.getSalary() ? e1 : e2);

System.out.println("\n公司月度薪资总额: " + totalSalary);
maxSalaryEmployee.ifPresent(emp -> 
    System.out.println("最高薪资员工: " + emp.getName() + " - " + emp.getSalary())
);


场景7:集合转换与去重

需求:获取所有不重复的部门列表,并转换为大写

List<String> departments = employees.stream()
    .map(Employee::getDepartment)
    .distinct() // 去重
    .map(String::toUpperCase) // 转换为大写
    .collect(Collectors.toList());

System.out.println("\n所有部门(大写): " + departments);


三、并行流处理

需求:并行处理大数据集,计算平均薪资


// 创建大型数据集
List<Employee> largeEmployeeList = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
    String dept = i % 3 == 0 ? "研发部" : (i % 3 == 1 ? "市场部" : "人事部");
    largeEmployeeList.add(new Employee("员工" + i, dept, 8000 + i % 10000));
}

// 顺序流
long startTime = System.currentTimeMillis();
double avgSalarySeq = largeEmployeeList.stream()
    .mapToDouble(Employee::getSalary)
    .average()
    .orElse(0);
long seqTime = System.currentTimeMillis() - startTime;

// 并行流
startTime = System.currentTimeMillis();
double avgSalaryPar = largeEmployeeList.parallelStream()
    .mapToDouble(Employee::getSalary)
    .average()
    .orElse(0);
long parTime = System.currentTimeMillis() - startTime;

System.out.println("\n大数据集处理结果:");
System.out.println("顺序流耗时: " + seqTime + "ms | 平均薪资: " + avgSalarySeq);
System.out.println("并行流耗时: " + parTime + "ms | 平均薪资: " + avgSalaryPar);


四、实战经验总结

最佳实践:



优先使用方法引用:使代码更简洁(如Employee::getDepartment)



避免状态干扰:Stream操作应避免修改外部状态



注意自动拆装箱:数值计算使用mapToInt/mapToDouble等



合理使用Optional:安全处理可能为空的结果



并行流使用原则:


数据量足够大(>10000元素)

无顺序依赖

操作足够耗时




性能考量:


小数据集:顺序流通常更快

复杂操作:并行流优势明显

短路操作(findFirst/anyMatch)优先使用


常见陷阱:

java

// 错误示例1:重复使用流
Stream<Integer> stream = Stream.of(1, 2, 3);
stream.forEach(System.out::println);
stream.forEach(System.out::println); // 抛出IllegalStateException

// 错误示例2:在流中修改外部变量
int[] sum = {0};
employees.stream().forEach(emp -> sum[0] += emp.getSalary()); // 非线程安全

// 正确方式
int total = employees.stream().mapToInt(Employee::getSalary).sum();


五、总结

Stream API通过声明式编程极大简化了集合操作,其核心优势在于:


代码简洁:减少样板代码

可读性强:链式调用直观表达数据处理流程

并行友好:轻松利用多核处理器

功能强大:支持复杂聚合操作


掌握Stream需要理解其操作分类(创建、中间、终端)和特性(惰性求值、不可复用)。本文展示的业务场景覆盖了日常开发中的大部分用例,建议结合自身项目实践,逐步替换传统循环操作,体验Stream带来的开发效率提升。


作者:天天摸鱼的java工程师

链接:https://juejin.cn