JDK8 Stream流保姆级教程:从入门到实战,告别繁琐遍历
在Java开发中,我们每天都要和集合、数组打交道,传统的for循环遍历、过滤、排序代码不仅冗长,还容易出现逻辑漏洞。而JDK8推出的Stream流,凭借Lambda表达式的加持,让数据处理代码变得简洁优雅、可读性拉满。今天就带大家从零吃透Stream流,从基础用法到实战案例一网打尽!
一、初识Stream流:告别传统遍历的臃肿
先来看一个经典场景:从姓名列表中筛选出姓张且名字为3个字的人,存入新集合。我们先对比传统方案和Stream流方案的代码差异。
1. 传统for循环方案
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流方案
// 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()方法:
List<String> list = new ArrayList<>();
Stream<String> s1 = list.stream();
2. Map集合获取Stream流
Map不直接实现Collection接口,需要先转成键集合、值集合或键值对集合,再获取流:
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()两种方式:
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()方法:
// 转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. 关键代码:牌的排序逻辑
private void sortCards(List<Card> cards) {
// 按牌的权重降序排列(权重num越大,牌越大)
Collections.sort(cards, (o1, o2) -> o2.getNum() - o1.getNum());
}
💡 技巧:这里用
Collections.sort的比较器,和Stream的sorted方法逻辑一致,体现了Java比较器的通用性。
五、进阶技巧:自定义对象排序
在Teacher.java中,我们看到如何为自定义对象实现排序:
@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. 自定义对象去重失败
// 错误:自定义对象默认无法去重
scores.stream().sorted().distinct().forEach(System.out::println);
解决方案:重写hashCode和equals方法:
@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. 流重复使用
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流重写你的集合操作代码了吗? 评论区留下你的实践心得,我们一起交流进步!