Java 8 Stream(建议收藏)
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 82w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2900+ 小伙伴加入学习 ,欢迎点击围观
在 Java 8 的版本更新中,引入的 Java 8 Stream 功能彻底改变了集合数据的处理方式。对于编程初学者和中级开发者来说,这是一个既强大又容易产生困惑的工具。本文将通过循序渐进的方式,结合实际案例,深入讲解 Java 8 Stream 的核心概念、操作方法以及常见问题,帮助读者快速掌握这一工具并将其应用于实际开发中。
什么是 Java 8 Stream?
Java 8 Stream(简称 Stream)是 Java 8 引入的一个用于集合数据处理的抽象模型。它可以将集合中的数据视为一个“数据流”,通过链式调用一系列操作(如过滤、映射、聚合等),高效地完成数据处理任务。
核心思想:流水线式处理
想象一个工厂的流水线:原材料经过多个加工步骤(如切割、打磨、包装),最终产出成品。Stream 的处理流程与此类似:
- 数据源:如集合(List、Set)或数组。
- 中间操作:对数据流进行加工(如筛选、转换)。
- 终端操作:触发计算并返回结果(如求和、收集到新集合)。
这种流水线设计的优势在于:
- 惰性求值:中间操作不会立即执行,而是等到终端操作时才开始计算。
- 函数式编程风格:通过组合操作链,代码更简洁易读。
Stream 的基本操作分类
Stream 的操作分为两类:中间操作和终端操作,它们的组合构成了完整的数据处理流程。
中间操作(Intermediate Operations)
特点:
- 返回一个新的 Stream 对象,不触发实际计算。
- 支持链式调用,可多次复用(如
filter().map().filter()
)。 - 惰性求值:只有在终端操作时才执行所有操作。
常用中间操作示例:
操作方法 | 功能描述 |
---|---|
filter() | 过滤符合条件的元素 |
map() | 将元素转换为其他形式 |
sorted() | 对元素进行排序 |
distinct() | 去重 |
比喻:流水线的加工步骤
假设我们有一个学生列表,想筛选出成绩大于 80 分的学生,并转换为姓名列表:
List<Student> students = ...
Stream<Student> filtered = students.stream()
.filter(s -> s.getScore() > 80) // 过滤步骤
.map(Student::getName); // 转换步骤
此时 filtered
只是定义了操作链,并未真正处理数据。
终端操作(Terminal Operations)
特点:
- 触发实际计算,返回结果或副作用(如打印输出)。
- 不可重复调用:每个 Stream 对象只能执行一次终端操作。
常用终端操作示例:
操作方法 | 功能描述 |
---|---|
forEach() | 遍历并执行操作(如打印) |
collect() | 将结果收集到集合中 |
reduce() | 聚合操作(如求和、求最大值) |
count() | 统计元素数量 |
示例:完成流水线并获取结果
List<String> names = students.stream()
.filter(s -> s.getScore() > 80)
.map(Student::getName)
.collect(Collectors.toList()); // 终端操作:收集为 List
Stream 的核心操作详解
1. 过滤数据:filter()
filter()
通过一个 Predicate(谓词)筛选元素。例如:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0) // 筛选偶数
.collect(Collectors.toList());
2. 数据转换:map()
map()
将每个元素转换为另一种类型。例如:
List<String> names = students.stream()
.map(Student::getName) // 将 Student 对象转为 String
.collect(Collectors.toList());
3. 数据聚合:reduce()
reduce()
是一个强大的聚合操作,支持三元形式:
Optional<Integer> sum = numbers.stream()
.reduce(0, (a, b) -> a + b); // 初始值 0,累加所有元素
- 初始值:聚合的起点(如求和时初始值为 0)。
- 二元操作:每次将当前结果与新元素合并。
Stream 的常见错误与解决方案
错误 1:忘记终端操作
students.stream().filter(s -> s.getScore() > 80); // 未触发计算!
解决方案:添加终端操作:
students.stream().filter(...).forEach(System.out::println);
错误 2:多次调用终端操作
Stream<Student> stream = students.stream();
stream.count(); // 第一次调用
stream.forEach(...); // 抛出 IllegalStateException
原因:Stream 对象只能使用一次。
解决方案:重新创建 Stream 或提前缓存结果。
错误 3:忽略惰性求值
Stream<Student> filtered = students.stream().filter(...);
// 在此处修改 students 集合,可能导致不可预测的结果
注意:Stream 的计算基于原始数据,若数据动态变化,需重新创建 Stream。
实战案例:学生平均分统计
假设我们需要统计所有成绩大于 70 分的学生的平均分:
步骤 1:定义数据
List<Student> students = Arrays.asList(
new Student("Alice", 85),
new Student("Bob", 65),
new Student("Charlie", 90)
);
步骤 2:构建 Stream 流程
double average = students.stream()
.filter(s -> s.getScore() > 70) // 过滤及格学生
.mapToDouble(Student::getScore) // 转为 Double Stream
.average() // 终端操作:计算平均值
.orElse(0.0); // 处理可能的空值
输出结果
System.out.println("平均分:" + average); // 输出 87.5
结论
Java 8 Stream 通过函数式编程的思想,为集合处理提供了优雅且高效的方式。通过理解中间操作与终端操作的协作、熟悉核心方法(如 filter()
、map()
、reduce()
),开发者可以快速编写出简洁且易维护的代码。
对于初学者,建议从简单案例入手(如过滤、统计),逐步尝试更复杂的操作(如分组、连接流)。同时,需注意 Stream 的惰性求值特性及常见错误,避免因误解导致的逻辑问题。掌握 Stream 后,开发者不仅能提升代码质量,还能更好地适应 Java 生态系统的函数式编程趋势。
通过本文的讲解与案例,希望读者能对 Java 8 Stream 有更全面的认识,并在实际项目中灵活运用这一强大工具。