Java 8 Stream API 实用技巧详解:从入门到精通

Java 8 的发布是 Java 发展史上的里程碑,其中 Stream API 的引入彻底改变了我们处理集合数据的方式。本文将结合实际开发中的常见场景,深入浅出地讲解 Stream API 的核心用法和最佳实践。


一、Stream API 基础概念

++什么是 Stream?++

Stream 不是数据结构,它不存储数据,而是用于对数据源(集合、数组等)进行高效、链式的函数式操作。Stream 操作分为中间操作(返回 Stream)和终端操作(返回结果)。

基本特点

  • 声明式编程:用更少的代码表达复杂的操作
  • 流水线操作:支持链式调用
  • 内部迭代:无需显式编写循环
  • 延迟执行:中间操作不会立即执行,遇到终端操作才触发

二、最常用的 Stream 操作集合

1. 求和操作(reduce)

处理金额、数量等数值字段时,求和是最常见的需求:

java 复制代码
// 对对象列表中的某个字段求和
BigDecimal total = vo.getInformationList().stream()
    .map(ContractPayInformation::getBidFeeAmount)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

代码解析

  • map() 将对象流转换为金额字段流
  • reduce()BigDecimal.ZERO 为初始值,通过 BigDecimal::add 累加
  • 即使列表为空也会返回 BigDecimal.ZERO,避免 NPE

2. 提取字段为集合(map + collect)

将对象列表转换为某个字段的列表:

java 复制代码
// 提取用户角色 ID 集合
List<Long> roleIds = user.getRoles().stream()
    .map(CurrentRoleDto::getId)
    .collect(Collectors.toList());

// 提取手机号并过滤空值
List<String> phones = list.stream()
    .map(YdbyCustom::getPhone)
    .filter(StringUtils::isNotEmpty)
    .collect(Collectors.toList());

最佳实践 :在提取字段后及时过滤 null 或空值,保证数据质量。


3. 去重操作(distinct / TreeSet)

方法一:使用 distinct()(基于 equals 和 hashCode)
java 复制代码
// 简单去重
List<Long> deviceIds = vos.stream()
    .map(SysDeviceStatusRecordVo::getDeviceId)
    .distinct()
    .collect(Collectors.toList());
方法二:基于对象特定字段去重

当需要根据对象的某个属性去重时,使用 TreeSet + Collectors.collectingAndThen

java 复制代码
// 根据 id 去重
List<Person> uniquePersons = persons.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toCollection(() -> new TreeSet<>(Comparator.comparingLong(Person::getId))),
        ArrayList::new
    ));

// 根据 name 去重
List<Person> uniqueByName = persons.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person::getName))),
        ArrayList::new
    ));

原理说明TreeSet 会按照指定 Comparator 去重,然后 collectingAndThen 将 TreeSet 转换为 ArrayList。


4. 条件过滤(filter)

查找单个元素
java 复制代码
List<Student> studentList = new ArrayList<>();
studentList.add(new Student(1, "张三", 90));
studentList.add(new Student(2, "李四", 60));
studentList.add(new Student(3, "王五", 30));
studentList.add(new Student(4, "赵六", 85));

// 根据 ID 查找学生,找不到返回 null
int studentId = 3;
Student student = studentList.stream()
    .filter(o -> o.getId() == studentId)
    .findAny()
    .orElse(null);
查找多个元素
java 复制代码
// 查找满足多个条件的对象集合
List<PanelGroupDTO> groupDTO = allList.stream()
    .filter(o -> o.getCreateBy().equals(AuthUtils.getUser().getUsername()) 
        && "folder".equals(o.getNodeType()))
    .collect(Collectors.toList());

findAny() vs findFirst()

  • findAny():返回任意匹配元素(并行流中性能更好)
  • findFirst():返回第一个匹配元素(保持顺序)

5. 对象转换(map)

将一个对象列表转换为另一种对象列表:

java 复制代码
// 使用 this::convert 方法引用
List<DynamicMenuDto> pluginDtos = pluginSysMenus.stream()
    .map(this::convert)
    .collect(Collectors.toList());

// 转换方法
private DynamicMenuDto convert(SysMenu sysMenu) {
    DynamicMenuDto dto = new DynamicMenuDto();
    dto.setId(sysMenu.getMenuId());
    dto.setPid(sysMenu.getPid());
    dto.setName(sysMenu.getMenuName());
    dto.setPath(sysMenu.getPath());
    return dto;
}

方法引用的优势:代码更简洁,可读性更强,逻辑复用性更好。


三、进阶技巧与最佳实践

1. 组合操作的顺序优化

java 复制代码
//  不推荐:先 map 后 filter,可能处理大量无效数据
list.stream()
    .map(ExpensiveObject::compute)
    .filter(Objects::nonNull)
    .collect(toList());

//  推荐:先 filter 过滤,再 map 转换
list.stream()
    .filter(Objects::nonNull)
    .map(ExpensiveObject::compute)
    .collect(toList());

2. 使用 Optional 处理可能为空的结果

java 复制代码
// 查找最大值,处理空集合情况
BigDecimal maxAmount = list.stream()
    .map(Order::getAmount)
    .max(BigDecimal::compareTo)
    .orElse(BigDecimal.ZERO);

3. 分组与统计

java 复制代码
// 按状态分组统计
Map<String, List<Order>> ordersByStatus = orders.stream()
    .collect(Collectors.groupingBy(Order::getStatus));

// 按分类统计数量
Map<Long, Long> countByCategory = products.stream()
    .collect(Collectors.groupingBy(Product::getCategoryId, Collectors.counting()));

// 按部门计算薪资总和
Map<String, BigDecimal> salaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.mapping(Employee::getSalary, Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))
    ));

4. 排序操作

java 复制代码
// 按金额升序
List<Order> sortedOrders = orders.stream()
    .sorted(Comparator.comparing(Order::getAmount))
    .collect(toList());

// 按金额降序,金额相同按时间排序
List<Order> sortedOrders = orders.stream()
    .sorted(Comparator.comparing(Order::getAmount)
        .reversed()
        .thenComparing(Order::getCreateTime))
    .collect(toList());

5. 匹配判断(anyMatch / allMatch / noneMatch)

java 复制代码
// 是否存在未完成订单
boolean hasPending = orders.stream()
    .anyMatch(o -> "PENDING".equals(o.getStatus()));

// 是否所有订单都已发货
boolean allShipped = orders.stream()
    .allMatch(o -> "SHIPPED".equals(o.getStatus()));

四、性能注意事项

1. 并行流的使用场景

java 复制代码
// 数据量大且操作独立时使用并行流
List<BigDecimal> result = largeList.parallelStream()
    .map(this::heavyComputation)
    .collect(toList());

何时使用并行流

  • 数据量很大(数万以上)
  • 元素处理相互独立(无状态依赖)
  • 操作是 CPU 密集型

何时避免并行流

  • 数据量小
  • 有状态操作(如修改共享变量)
  • 顺序依赖的操作

2. 避免频繁装箱

java 复制代码
//  可能产生大量装箱操作
IntStream.range(1, 10000)
    .boxed()
    .mapToInt(i -> i)
    .sum();

//  使用原始类型流
IntStream.range(1, 10000)
    .sum();

五、完整实战示例

场景:订单统计报表

java 复制代码
public class OrderReportService {
    
    public OrderReport generateReport(List<Order> orders) {
        // 1. 过滤有效订单
        List<Order> validOrders = orders.stream()
            .filter(o -> "PAID".equals(o.getStatus()))
            .filter(o -> o.getAmount().compareTo(BigDecimal.ZERO) > 0)
            .collect(toList());
        
        // 2. 计算总金额
        BigDecimal totalAmount = validOrders.stream()
            .map(Order::getAmount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
        
        // 3. 按用户分组统计
        Map<Long, List<Order>> userOrders = validOrders.stream()
            .collect(Collectors.groupingBy(Order::getUserId));
        
        // 4. 找出最大订单
        Order maxOrder = validOrders.stream()
            .max(Comparator.comparing(Order::getAmount))
            .orElse(null);
        
        // 5. 提取所有商品 ID 列表(去重)
        List<Long> productIds = validOrders.stream()
            .flatMap(o -> o.getProductIds().stream())
            .distinct()
            .collect(toList());
        
        // 6. 统计订单数量
        long orderCount = validOrders.size();
        
        return new OrderReport(totalAmount, orderCount, userOrders.size(),
                               maxOrder, productIds);
    }
}

六、总结

核心要点速记

操作 方法 说明
遍历 forEach() 终端操作,消费每个元素
转换 map() 将元素转换为另一种形式
过滤 filter() 保留满足条件的元素
去重 distinct() / TreeSet 去除重复元素
排序 sorted() 对元素排序
聚合 reduce() / collect() 将流归约为单个值
查找 findAny() / findFirst() 查找匹配元素
匹配 anyMatch() / allMatch() 检查是否匹配条件

开发建议

  1. 优先使用 Stream API 替代传统 for 循环,代码更清晰
  2. 注意空指针保护 :使用 Optionalfilter(Objects::nonNull)
  3. 合理使用并行流:充分测试后再使用
  4. 保持操作链简洁:避免过长的链式调用(建议不超过 5 个操作)
  5. 复用方法引用:将复杂转换逻辑抽取为独立方法
相关推荐
冻柠檬飞冰走茶1 小时前
PTA基础编程题目集 7-27 冒泡法排序(C语言实现)
c语言·开发语言·数据结构·算法
听雨入夜1 小时前
“同声传译”还是“全文翻译”?为何HotSpot虚拟机仍要保留解释器?
开发语言·python
随风M记忆s1 小时前
GEE&Python-demo:利用Sentinel-监测北京奥林匹克森林公园年NDVI变化(附Python版)
开发语言·python·sentinel
C++、Java和Python的菜鸟2 小时前
第9章 后端Web进阶(AOP)
java·开发语言·前端
软萌萌的12 小时前
Python零基础从入门到精通详细教程-数据类型
开发语言·windows·python
东北赵四2 小时前
关于Java泛型的知识点及其相关面试题
java·windows·python
姓蔡小朋友2 小时前
Java线程并发
java·开发语言·python
执笔画流年呀2 小时前
⾃动化测试常⽤函数
java·dubbo
我星期八休息2 小时前
扩展— TCP 全连接队列与 tcpdump 抓包
linux·服务器·开发语言·前端·网络·tcp/ip·tcpdump