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() |
检查是否匹配条件 |
开发建议
- 优先使用 Stream API 替代传统 for 循环,代码更清晰
- 注意空指针保护 :使用
Optional或filter(Objects::nonNull) - 合理使用并行流:充分测试后再使用
- 保持操作链简洁:避免过长的链式调用(建议不超过 5 个操作)
- 复用方法引用:将复杂转换逻辑抽取为独立方法