JDK8 Stream流保姆级教程:从入门到实战,告别繁琐遍历
在Java开发中,我们每天都要和集合、数组打交道,传统的for循环遍历、过滤、排序代码不仅冗长,还容易出现逻辑漏洞。而JDK8推出的Stream流,凭借Lambda表达式的加持,让数据处理代码变得简洁优雅、可读性拉满。今天就带大家从零吃透Stream流,从基础用法到实战案例一网打尽!
一、初识Stream流:告别传统遍历的臃肿
先来看一个经典场景:从姓名列表中筛选出姓张且名字为3个字的人,存入新集合。我们先对比传统方案和Stream流方案的代码差异。
1. 传统for循环方案
csharp
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
list.add("张翠山");
// 传统筛选逻辑
List<String> newlist = new ArrayList<>();
for (String name : list) {
if (name.startsWith("张") && name.length() == 3) {
newlist.add(name);
}
}
System.out.println(newlist); // [张无忌, 张三丰, 张翠山]
2. Stream流方案
scss
// Stream流链式调用
List<String> newlist2 = list.stream()
.filter(name -> name.startsWith("张"))
.filter(name -> name.length() == 3)
.collect(Collectors.toList());
System.out.println(newlist2); // [张无忌, 张三丰, 张翠山]
关键点 :Stream流是惰性求值 的,只有在终结方法(如forEach、count)被调用时才会执行操作,这大大提高了效率。
Stream流的核心优势
- 代码简洁:Lambda语法+链式调用,告别嵌套循环
- 性能高效:底层实现了并行处理机制,适合大数据量操作
- 可读性强:方法名直观(filter、sorted等),业务逻辑清晰
- 惰性求值:只在需要结果时才执行操作,避免不必要的计算
二、获取Stream流:3种数据源的获取方式
Stream流的操作第一步是获取流 ,不同数据源(集合、Map、数组)有不同的获取方式,对应代码在StreamDemo2.java中。
1. 集合获取Stream流
所有Collection接口的实现类(ArrayList、HashSet等),都可以直接调用stream()方法:
ini
List<String> list = new ArrayList<>();
Stream<String> s1 = list.stream();
2. Map集合获取Stream流
Map不直接实现Collection接口,需要先转成键集合、值集合或键值对集合,再获取流:
arduino
Map<String, Integer> map = new HashMap<>();
// 键流
Stream<String> keyStream = map.keySet().stream();
// 值流
Stream<Integer> valueStream = map.values().stream();
// 键值对流
Stream<Map.Entry<String, Integer>> entryStream = map.entrySet().stream();
3. 数组获取Stream流
可以通过Arrays.stream()或Stream.of()两种方式:
ini
String[] names = {"张三丰", "张无忌", "张翠山", "张良", "张学友"};
// 方式1:Arrays工具类
Stream<String> s5 = Arrays.stream(names);
// 方式2:Stream静态方法
Stream<String> s6 = Stream.of(names);
Stream<String> s7 = Stream.of("张三丰", "张无忌", "张翠山"); // 直接传元素
三、Stream流核心操作:中间方法+终结方法
Stream流的操作分为中间方法 和终结方法,中间方法支持链式调用,终结方法会终止流并返回结果。
1. 中间方法:数据处理的"流水线"
中间方法调用后会返回新的Stream流,可继续链式操作,核心方法如下:
| 方法 | 作用 | 示例 | 说明 |
|---|---|---|---|
filter() |
过滤数据 | filter(name -> name.startsWith("张")) |
筛选符合条件的数据 |
sorted() |
排序 | sorted()/sorted((a,b)->b-a) |
默认升序,自定义降序 |
limit(n) |
取前n个元素 | sorted().limit(2) |
取前2个元素 |
skip(n) |
跳过前n个元素 | sorted().skip(2) |
跳过前2个元素 |
distinct() |
去重 | distinct() |
自定义对象需重写hashCode和equals |
map() |
数据加工 | map(score -> score + 10) |
将数据转换为新形式 |
concat() |
合并流 | Stream.concat(s1, s2) |
合并两个流 |
✅ 注意 :
distinct()去重对于自定义对象(如Teacher)需要重写hashCode和equals方法才能生效。
2. 终结方法:数据处理的"终点站"
终结方法调用后流会关闭,无法再操作,核心方法如下:
| 方法 | 作用 | 示例 |
|---|---|---|
forEach() |
遍历元素 | filter(t -> t.getSalary() > 17000).forEach(System.out::println) |
count() |
统计元素个数 | filter(t -> t.getSalary() > 17000).count() |
max()/min() |
获取最大值/最小值 | max((t1,t2)->Double.compare(t1.getSalary(),t2.getSalary())) |
collect() |
收集结果 | collect(Collectors.toList()) |
3. 收集Stream流:转回集合/数组
Stream流是"临时工具",最终需要将处理结果转回集合或数组,核心用collect()方法:
rust
// 转List
List<String> list1 = list.stream()
.filter(name -> name.startsWith("张"))
.collect(Collectors.toList());
// 转Set(自动去重)
Set<String> set = list.stream()
.filter(name -> name.startsWith("张"))
.collect(Collectors.toSet());
// 转数组
String[] arr = list.stream()
.filter(name -> name.startsWith("张"))
.toArray(String[]::new);
// 转Map(key为老师名,value为薪水)
Map<String, Double> teacherMap = teachers.stream()
.collect(Collectors.toMap(Teacher::getName, Teacher::getSalary));
⚠️ 重要提示 :一个Stream流只能被收集一次,重复收集会报错!
StreamDemo4.java中明确展示了这一点。
四、实战案例:用Stream+Collections实现斗地主游戏
理论学完,我们用一个斗地主游戏实战,结合Stream流和Collections工具类,实现洗牌、发牌、排序的完整流程。
1. 核心流程
- 准备牌 :定义
Card类封装牌的点数、花色、权重,初始化54张牌 - 洗牌 :用
Collections.shuffle()打乱牌序 - 发牌:循环分发51张牌,剩余3张作为底牌
- 排序 :用
Collections.sort()结合自定义比较器,按牌权重降序排列 - 抢地主:将底牌分给地主玩家
2. 关键代码:牌的排序逻辑
scss
private void sortCards(List<Card> cards) {
// 按牌的权重降序排列(权重num越大,牌越大)
Collections.sort(cards, (o1, o2) -> o2.getNum() - o1.getNum());
}
💡 技巧 :这里用
Collections.sort的比较器,和Stream的sorted方法逻辑一致,体现了Java比较器的通用性。
五、进阶技巧:自定义对象排序
在Teacher.java中,我们看到如何为自定义对象实现排序:
less
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Teacher implements Comparable<Teacher> {
private String name;
private int age;
private double salary;
@Override
public int compareTo(Teacher o) {
// 按年龄升序
return this.age - o.age;
// return o.age - this.age; // 降序
}
}
重要提示 :如果希望使用sorted()方法对自定义对象排序,必须实现Comparable接口,或提供自定义比较器。
六、常见错误与解决方案
1. 自定义对象去重失败
scss
// 错误:自定义对象默认无法去重
scores.stream().sorted().distinct().forEach(System.out::println);
解决方案 :重写hashCode和equals方法:
kotlin
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Teacher teacher = (Teacher) o;
return age == teacher.age && Double.compare(teacher.salary, salary) == 0 && Objects.equals(name, teacher.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age, salary);
}
2. 流重复使用
ini
Stream<String> s1 = list.stream().filter(name -> name.startsWith("张"));
List<String> list1 = s1.collect(Collectors.toList());
// 重复使用s1会抛出异常
List<String> list2 = s1.collect(Collectors.toList());
解决方案:每个Stream流只能被收集一次,如需多次使用,应重新获取流。
七、学习总结
Stream流是JDK8的核心特性之一,它不是替代集合/数组,而是操作集合/数组的高效工具:
- 获取流:从集合、Map、数组中获取Stream流
- 中间方法 :
filter、sorted、map等,支持链式调用 - 终结方法 :
forEach、count、collect等,触发实际计算 - 惰性求值:只有在终结方法被调用时才执行操作
- 与Lambda结合:让代码更简洁,业务逻辑更清晰
掌握Stream流,能大幅提升集合操作的开发效率,告别"屎山"遍历代码!
写在最后
如果你也被传统集合遍历折磨过,不妨试试Stream流,相信会打开Java数据处理的新世界。后续可以多尝试用Stream重构旧代码,感受它的便捷性!
附:Stream流常用方法速查表
| 类别 | 方法 | 说明 |
|---|---|---|
| 获取流 | list.stream() |
集合获取流 |
Arrays.stream(arr) |
数组获取流 | |
Stream.of(...) |
静态方法获取流 | |
| 中间方法 | filter() |
过滤数据 |
sorted() |
排序 | |
map() |
转换数据 | |
limit() |
取前n个 | |
skip() |
跳过前n个 | |
distinct() |
去重 | |
| 终结方法 | forEach() |
遍历 |
count() |
统计 | |
max()/min() |
获取最大/最小值 | |
collect() |
收集结果 | |
toArray() |
转数组 |
💡 小贴士 :在IDE中,Stream流的链式调用可以按
Ctrl+Enter换行,让代码更易读。
现在,你准备好用Stream流重写你的集合操作代码了吗? 评论区留下你的实践心得,我们一起交流进步!