【JAVA】Java8的 Stream相关学习分析总结
Stream 操作按 “核心需求场景” 分类归纳,每个类别下整合 “操作目的、核心方法、案例代码、关键说明”
四个基本语法概念:
Stream:的操作是链式执行的,每个操作都会基于上一步的结果生成新的流。
map:将流中的每个元素按照指定的规则(函数)进行转换,生成一个包含转换后元素的新流。
collect() 方法是一个终端操作, “执行收集动作” 的入口,而具体 “收集成什么样子”,由传入的 Collector(收集器)决定。
另一篇文章的补充(例子更清晰一些):https://blog.csdn.net/ew45218/article/details/144903267
// 创建一个学生列表
List<Student> students = Arrays.asList(new Student("Alice", 22, "Computer Science"),new Student("Bob", 21, "Mathematics"),new Student("Charlie", 23, "Chemistry"),new Student("David", 19, "Biology"),new Student("Eva", 20, "Mathematics"),new Student("Frank", 22, "History"),new Student("Grace", 21, "Biology")
);
一、基础筛选:从列表中 “挑出” 符合条件的元素
核心需求:过滤元素、判断元素是否存在(不改变元素本身,只筛选保留 / 判断)关键方法:filter
(过滤元素,中间操作)、anyMatch/allMatch/noneMatch
(判断匹配,终端操作)
操作目的 | 案例代码 | 关键说明 |
---|---|---|
过滤符合条件的对象 | List<Student> collect = students.stream().filter(s -> s.age > 20).collect(Collectors.toList()); | 1. filter 是中间操作,需搭配 collect 终端操作才会执行;2. 筛选逻辑通过 Lambda 定义(这里是 “年龄> 20”)。 |
判断是否存在符合条件元素 | boolean hasUnderage = students.stream().anyMatch(s -> s.age < 18); | 1. anyMatch 是终端操作,直接返回 boolean ;2. 短路操作:找到第一个符合条件的元素就停止,效率高。 |
二、元素转换:改变元素的 “形式” 或 “内容”
核心需求:提取对象属性、修改属性内容(将流中元素转换为新形式,生成新流)关键方法:map
(元素转换,中间操作)
操作目的 | 案例代码 | 关键说明 |
---|---|---|
提取对象的某个属性 | List<String> collect3 = students.stream().map(Student::getMajor).distinct().collect(Collectors.toList()); | 1. map(Student::getMajor) 将 Student 对象转为 String 类型的专业;2. 转换后流的元素类型从 Student 变为 String 。 |
修改属性内容 | List<String> collect1 = students.stream().map(s -> s.getName().toUpperCase()).collect(Collectors.toList()); | 1. 这里将 “学生姓名” 转换为大写;2. 转换逻辑可自定义(如拼接字符串、数值计算等)。 |
三、排序:对列表元素按规则 “排顺序”
核心需求:按对象属性升序 / 降序排列关键方法:sorted
(排序,中间操作)+ Comparator
(比较器)
操作目的 | 案例代码 | 关键说明 |
---|---|---|
按属性倒序排序 | List<Student> collect2 = students.stream().sorted(Comparator.comparingInt(Student::getAge).reversed()).collect(Collectors.toList()); | 1. Comparator.comparingInt(Student::getAge) 定义 “按年龄正序”;2. reversed() 反转顺序,实现倒序;3. 排序后仍保留完整 Student 对象。 |
四、统计计算:对数值类属性做 “聚合计算”
核心需求:求和、求平均、计数(针对数值型数据,得到单一结果)关键方法:mapToInt
(转为数值流,中间操作)、sum/average
(终端操作)、reduce
(通用聚合,终端操作)
操作目的 | 案例代码 | 关键说明 |
---|---|---|
求数值总和(简单场景) | int sum = students.stream().mapToInt(Student::getAge).sum(); | 1. mapToInt 转为 IntStream ,避免装箱 / 拆箱,性能高;2. sum() 是 IntStream 专用终端操作,直接返回总和。 |
求数值总和(自定义场景) | int totalAge = students.stream().map(Student::getAge).reduce(0, (a, b) -> a + b); | 1. reduce 是通用聚合方法,适合复杂逻辑(如带条件的累加);2. 第一个参数是初始值(求和初始为 0),第二个是累加器(a 是当前和,b 是下一个元素)。 |
求平均值 | OptionalDouble average = students.stream().mapToInt(Student::getAge).average(); | 1. 返回 OptionalDouble ,避免流为空时返回 null ;2. 可通过 average.getAsDouble() 获取具体值(需确保流非空)。 |
基础计数(列表大小) | int size = students.size(); | 1. 直接调用 List 的 size() 方法,适合无需 Stream 其他操作的简单计数;2. 若需先过滤再计数,可用 students.stream().filter(...).count() 。 |
五、去重:消除 “重复” 元素(按属性 / 对象)
核心需求:按属性去重(保留属性列表)、按属性去重(保留原始对象)关键方法:distinct
(简单去重,中间操作)、Collectors.toMap
(按属性去重,终端操作)
操作目的 | 案例代码 | 关键说明 |
---|---|---|
按属性去重(得到属性列表) | List<String> collect3 = students.stream().map(Student::getMajor).distinct().collect(Collectors.toList()); | 1. 先通过 map 提取专业属性,再用 distinct 去重;2. 最终得到 “无重复的专业列表”(List<String> )。 |
按属性去重(保留原始对象) | List<Student> uniqueByMajor = students.stream().collect(Collectors.toMap(Student::getMajor, s->s, (e,n)->e)).values().stream().collect(Collectors.toList()); | 1. 利用 Map 的 key 唯一特性:key 是专业,value 是 Student 对象;2. 第三个参数 (e,n)->e 表示 “重复时保留旧对象”;3. 最后通过 values() 提取去重后的 Student 对象。 |
六、分组与聚合:按规则 “归类” 元素,或合并元素
核心需求:按属性分组、合并元素(如拼接字符串)关键方法:Collectors.groupingBy
(分组,终端操作)、Collectors.joining
(拼接,终端操作)、reduce
(合并,终端操作)
操作目的 | 案例代码 | 关键说明 |
---|---|---|
按属性分组 | Map<String, List<Student>> collect5 = students.stream().collect(Collectors.groupingBy(Student::getMajor)); | 1. 按 “专业” 分组,key 是专业,value 是该专业的所有 Student 对象;2. 适合 “分类统计” 场景(如 “每个专业有多少学生”)。 |
合并元素为字符串 | String collect4 = students.stream().map(Student::getName).collect(Collectors.joining(",")); | 1. 先提取姓名,再用 joining(",") 按 “逗号” 拼接;2. 无需手动处理循环,直接得到合并后的字符串。 |
合并元素(通用场景) | String reduce1 = students.stream().map(Student::getMajor).reduce("", (a, b) -> a + b); | 1. reduce 也可用于字符串拼接(初始值为空串,累加器是字符串拼接);2. 相比 joining ,reduce 更灵活,但 joining 对字符串场景更简洁。 |
总结:Stream 操作的核心逻辑(3 步)
- 明确需求:先想清楚要做什么(过滤?转换?统计?);
- 选中间操作:用
filter/map/sorted
等定义 “处理规则”(生成新流,不执行); - 选终端操作:用
collect/sum/anyMatch
等触发 “执行并得到结果”(终端操作只能有一个)。
Stream 的核心特性(避免误用)、常见坑点(避坑指南)、Optional 配合使用(安全处理空值)
一、Stream 核心特性:必须记住的 “规则”
这些特性决定了 Stream 的使用边界,不注意就会出问题:
1. 不可重复消费(Stream 是 “一次性” 的)
- 问题:一个 Stream 对象只能执行一次终端操作(如
collect
、sum
、anyMatch
),执行后流会被 “消耗”,再次调用终端操作会报错。 - 示例:
Stream<Student> stream = students.stream().filter(s -> s.age > 20); stream.collect(Collectors.toList()); // 第一次终端操作:正常 stream.count(); // 第二次终端操作:报错(IllegalStateException: stream has already been operated upon or closed)
- 解决:每次需要处理时,重新生成流(如
students.stream()
每次调用都会创建新流)。
2. 并行流(Parallel Stream):高效处理大数据,但需注意线程安全
- 适用场景:数据量极大(如上万条数据),想利用多核 CPU 加速处理时,可将普通流转为并行流。
- 用法:在
stream()
后加parallel()
,或直接用parallelStream()
:// 并行流计算年龄总和(数据量大时比普通流快) int totalAge = students.parallelStream().mapToInt(Student::getAge).sum();
- 坑点:并行流会多线程处理元素,若操作中涉及 线程不安全的集合(如
ArrayList
),会导致数据错误:// 错误示例:ArrayList 线程不安全,并行流添加元素会丢数据 List<String> names = new ArrayList<>(); students.parallelStream().map(Student::getName).forEach(names::add); // 可能出现元素丢失或数组越界// 正确示例:用线程安全的集合,或直接 collect List<String> names = students.parallelStream().map(Student::getName).collect(Collectors.toList()); // collect 内部会处理线程安全
二、常见坑点:避开这些 “隐形错误”
1. 空指针问题(流中存在 null 元素)
- 问题:若流中包含
null
对象,后续map
、filter
操作调用对象方法时,会直接抛出NullPointerException
。 - 示例:
// 假设 students 中混入一个 null(如从数据库查询时可能出现) List<Student> students = Arrays.asList(new Student("Alice",22,"CS"), null, new Student("Bob",21,"Math"));// 错误:调用 null.getName() 会抛空指针 students.stream().map(Student::getName).collect(Collectors.toList());// 解决:先过滤掉 null 元素 students.stream().filter(Objects::nonNull) // 过滤 null,Objects::nonNull 是方法引用,等价于 s -> s != null.map(Student::getName).collect(Collectors.toList());
2. sorted
排序的 “隐性要求”
- 问题:若对自定义对象(如
Student
)用sorted()
且未指定比较器,会抛出ClassCastException
—— 因为Student
未实现Comparable
接口。 - 示例:
// 错误:Student 未实现 Comparable,直接 sorted() 会报错 students.stream().sorted().collect(Collectors.toList());// 正确:必须指定比较器(如按年龄排序) students.stream().sorted(Comparator.comparingInt(Student::getAge)).collect(Collectors.toList());
3. Collectors.toMap
的 “键重复” 问题
- 问题:用
Collectors.toMap
时,若流中存在 重复的 key(如两个学生专业相同),且未指定 “重复键处理规则”,会抛出IllegalStateException
。 - 示例:
// 错误:若有两个学生专业都是 "Mathematics",会抛键重复异常 Map<String, Student> map = students.stream().collect(Collectors.toMap(Student::getMajor, s -> s)); // 无重复处理规则// 正确:指定重复键时保留旧值或新值 Map<String, Student> map = students.stream().collect(Collectors.toMap(Student::getMajor, s -> s, (oldVal, newVal) -> oldVal // 重复时保留旧值;若想保留新值,改写成 (o,n) -> n));
三、Optional 配合使用:安全处理 “无结果” 场景
之前的 average()
、reduce()
会返回 Optional
类型(如 OptionalDouble
、Optional<Integer>
),其核心作用是 避免空指针,安全处理 “流为空时无结果” 的情况。
错误用法:直接 get()
,可能抛异常
// 错误:若 students 为空,average() 返回 OptionalDouble.empty,getAsDouble() 会抛 NoSuchElementException
double avg = students.stream().mapToInt(Student::getAge).average().getAsDouble();
正确用法:用 orElse
、ifPresent
等方法处理空值
方法 | 作用 | 示例代码 |
---|---|---|
orElse(默认值) | 无结果时返回默认值 | double avg = students.stream().mapToInt(Student::getAge).average().orElse(0.0); |
orElseGet(供应器) | 无结果时通过函数生成默认值(更灵活) | double avg = students.stream().mapToInt(Student::getAge).average().orElseGet(() -> 0.0); |
ifPresent(消费者) | 有结果时才执行操作(避免判断 null) | students.stream().mapToInt(Student::getAge).average().ifPresent(avg -> System.out.println("平均年龄:" + avg)); |
四、补充小技巧:提升开发效率
1. 链式操作的 “最优顺序”:先过滤,再转换 / 排序
- 原理:
filter
会减少流中元素的数量,后续的map
、sorted
等操作处理的数据量减少,效率更高。 - 示例:
// 推荐:先过滤(年龄>20),再提取姓名(减少 map 处理的元素) List<String> names = students.stream().filter(s -> s.age > 20).map(Student::getName).collect(Collectors.toList());// 不推荐:先 map 再 filter,多处理了年龄≤20的元素 List<String> names = students.stream().map(Student::getName).filter(name -> students.stream().filter(s -> s.getName().equals(name)).findFirst().get().age > 20).collect(Collectors.toList());
2. 用 peek
调试 Stream 流程
- 作用:
peek
是中间操作,可在流的每个元素处理时 “插入调试逻辑”(如打印元素),不改变元素本身,方便定位问题。 - 示例:
// 查看过滤、转换的中间结果 students.stream().peek(s -> System.out.println("原始元素:" + s.getName())) // 打印原始元素.filter(s -> s.age > 20).peek(s -> System.out.println("过滤后元素:" + s.getName())) // 打印过滤后元素.map(Student::getName).peek(name -> System.out.println("转换后元素:" + name)) // 打印转换后元素.collect(Collectors.toList());