Java中关于 Stream 学习笔记
什么是Stream
Stream 可以理解成一条数据处理流水线。
它不是直接去操作集合本身,而是:先把集合变成流,再对流里的元素做一系列处理,最后得到结果
核心思路:原始数据 -> 加工处理 -> 得到结果
1 | 集合.stream() |
常见处理流程
最常见的写法:1
2
3
4
5
6
7List<String> result = list.stream() // 先从集合开始
.filter(Objects::nonNull) // 【过滤数据】去掉`null`
.map(String::valueOf) // 【转换数据】转成字符串
.distinct() // 去重
.sorted() // 排序
.limit(10) // 取前 10 个
.collect(Collectors.toList()); // 收集成新集合
函数介绍
Stream核心流程:流进来 -> 处理 -> 收集结果
stream | filter | 筛选 | |||
map | 转换 | ||||
forEach | 执行动作 | ||||
sorted | 排序 | ||||
distinct | 去重 | ||||
skip | 跳过 | ||||
limit | 截取 | ||||
count | 计数 | ||||
findFirst | 找第一个 | ||||
collect | 收集 | ||||
toList | 收集成列表 | ||||
toSet | 收集成去重集合 | ||||
toMap | 收集成键值对 | ||||
groupingBy | 按字段分组 | ||||
counting() | 每组数量 | ||||
mapping() | 每组提取字段 | ||||
summingInt() | 每组求和 | ||||
Stream 执行顺序
这点很重要。
不是先把所有元素都 map,再把所有元素都 filter。而是一个元素一个元素地往下走。1
2
3
4list.stream()
.filter(...)
.map(...)
.collect(...)
执行方式更像:
- 第一个元素先过
filter - 如果通过,再过
map - 然后处理下一个元素输出顺序会更像:
1
2
3
4
5
6
7
8
9
10
11List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
List<String> result = numbers.stream()
.filter(n -> {
System.out.println("filter: " + n);
return n % 2 == 0;
})
.map(n -> {
System.out.println("map: " + n);
return "数字" + n;
})
.collect(Collectors.toList());1
2
3
4
5
6filter: 1
filter: 2
map: 2
filter: 3
filter: 4
map: 4
map、filter、forEach
分别是干什么的
map的作用是:把一个元素转换成另一个元素。一句话理解:一个元素进来,一个新元素出去1
2
3
4// 这里是把数字转换成字符串。
List<String> list = numbers.stream()
.map(String::valueOf)
.collect(Collectors.toList());filter的作用是:判断这个元素要不要保留。一句话理解:留还是不留1
2
3
4
5
6// 这里表示:
// - 偶数保留
// - 奇数过滤掉
List<Integer> list = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());forEach的作用是:对每个元素执行一个动作。一句话理解:拿每个元素做点事1
2// 这里就是把每个元素打印出来。
numbers.stream().forEach(System.out::println);到底返回什么
map:返回新元素,转换后的新值1
2
3
4// 这里返回的是 `String`
.map(n -> n.toString())
// 这里返回的是用户名
.map(user -> user.getName())filter:返回布尔值,true或false1
2// `true`:保留,`false`:过滤掉
.filter(n -> n > 10)forEach不需要返回值。1
2// 这里只是做动作,不返回结果。
.forEach(n -> System.out.println(n))
lambda 加 {} 后是不是就必须 return
不完全是。更准确地说,如果这个 lambda 本来就需要返回值,那么加了 {} 后就要自己写 return。
map加{}需要return,map需要返回新值。1
2
3.map(o -> {
return o.toString();
})filter加{}需要return,filter需要返回布尔值。1
2
3.filter(o -> {
return o != null;
})forEach加{}不需要return,forEach本来就不需要返回值。1
2
3.forEach(o -> {
System.out.println(o);
})
String::valueOf 和 o -> o.toString() 区别
这两个通常都能把对象转成字符串。1
2
3
4// 写法一
.map(String::valueOf)
// 写法二
.map(o -> o.toString())
最大区别:null
o -> o.toString(),如果o是null,会报空指针异常。String::valueOf,如果是null,不会报错,会返回字符串"null"。
更安全的写法1
2
3
4
5
6
7//方案 1:先判断 null
.map(o -> o == null ? null : o.toString())
// 方案 2:给 null 默认值
.map(o -> o == null ? "" : o.toString())
// 方案 3:先过滤 null
.filter(Objects::nonNull)
.map(Object::toString)
为什么一般建议先 filter 再 map
因为先过滤掉不需要的数据,后面的转换就会少做很多事。1
2
3
4
5// 这里先保留偶数,再转字符串,更合理。
numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> "数字" + n)
.collect(Collectors.toList());
好处:
- 性能更好,因为
map只处理留下来的元素。 - 逻辑更清晰,更符合“先筛选,再加工”的思路。
- 更安全,比如先过滤
null,再toString(),就不会空指针。
如果过滤条件依赖转换后的结果,就只能先 map。1
2
3
4
5// 必须先变成字符串,才能判断字符串长度。
numbers.stream()
.map(String::valueOf)
.filter(s -> s.length() == 1)
.collect(Collectors.toList());
sorted、distinct、limit 放前放后有什么区别
顺序不同,结果可能不同。
sorted().limit(3)和limit(3).sorted()不一样1
2
3
4
5
6
7
8
9
10// 先排序再取前3个(取最小的3个)
list.stream()
.sorted()
.limit(3)
.collect(Collectors.toList());
// 先取前3个再排序(原始前3个元素里排个序)
list.stream()
.limit(3)
.sorted()
.collect(Collectors.toList());distinct().limit(3)和limit(3).distinct()不一样
distinct().sorted()和sorted().distinct()
最终结果有时一样,但一般先去重后排序,先去重后,排序的数据量可能更少1
.distinct().sorted()
skip() 和 limit() 使用
最常见就是分页。
skip(n):跳过前n个元素。limit(n):取前n个元素。
1 | // 跳过前 `a` 个,再取接下来的 `b` 个 |
分页公式1
2.skip((pageNum - 1) * pageSize)
.limit(pageSize)1
2
3
4
5
6
7
8// 跳过前3个,再取4个
List<Integer> result = list.stream()
.skip(3)
.limit(4)
.collect(Collectors.toList());
// 第3页,每页10条:
.skip(10).limit(10)
forEach:list.forEach 和 list.stream().forEach 区别
list.forEach(...),集合本身的方法。直接遍历这个集合中的每个元素,它不经过Stream流水线1
list.forEach(System.out::println);
list.stream().forEach(...)先把集合转成 Stream,再调用 Stream 的终止操作。也是遍历输出,但它是 Stream 的写法它们什么时候看起来一样,如果只是简单遍历打印,效果通常一样1
list.stream().forEach(System.out::println);
真正区别1
2
3list.forEach(System.out::println);
list.stream().forEach(System.out::println);list.forEach(...),直接遍历
单纯遍历,不需要中间操作,1
list.forEach(System.out::println);
list.stream().forEach(...),先走 Stream 流水线,最后再遍历处理
前面还要filter,还要map,还要sorted怎么选1
2
3
4list.stream()
.filter(Objects::nonNull)
.map(String::valueOf)
.forEach(System.out::println);1
2
3
4
5
6
7// 只想遍历集合
list.forEach(...)
// 想先处理再遍历
```java
list.stream()
.xxx()
.forEach(...)
终止操作:count()、findFirst()、anyMatch() 等
count(),统计数量
适合场景:有多少条;足条件的有多少个1
2
3long count = numbers.stream()
.filter(n -> n > 2)
.count();findFirst(),找到第一个符合条件的元素。返回的是Optional<T>,常配合orElse(...)使用。
适合场景:找第一个符合条件的元素,找到一个就够了1
2
3
4Integer value = numbers.stream()
.filter(n -> n > 2)
.findFirst()
.orElse(null);anyMatch(),是否存在任意一个满足条件
适合场景:有没有空值,有没有异常数据,有没有符合条件的元素1
2boolean has = numbers.stream()
.anyMatch(n -> n > 10);allMatch(),是否全部满足条件1
2boolean all = numbers.stream()
.allMatch(n -> n > 0);noneMatch(),是否一个都不满足记忆方式,这些方法会真正触发 Stream 执行。1
2boolean none = numbers.stream()
.noneMatch(n -> n < 0);
count():几个findFirst():第一个是谁anyMatch():有没有allMatch():是不是全部noneMatch():是不是一个都没有
collect(...) 是做什么的
collect(...):把 Stream 处理后的结果,收集成你想要的容器
最常见的有:toList()、toSet()、toMap()
Collectors.toList()
作用:收集成 List
特点:保留顺序,允许重复1
List<String> list = stream.collect(Collectors.toList());
Collectors.toSet()
作用:收集成 Set
特点:自动去重,不要依赖顺序1
Set<String> set = stream.collect(Collectors.toSet());
Collectors.toMap()
作用:收集成 Map
表示:key:id,value:name1
2
3
4
5Map<Integer, String> map = users.stream()
.collect(Collectors.toMap(
User::getId,
User::getName
));如何解决?注意:
toMap()默认key不能重复。如果重复会报错。1
2
3
4
5
6Map<Integer, String> map = list.stream()
.collect(Collectors.toMap(
keyMapper,
valueMapper,
(oldValue, newValue) -> oldValue // 表示 key 冲突时保留谁
));
toMap 和 groupingBy 区别
toMap:一个 key 对一个 value。给每个 key 找一个值。适合“建立索引”。1
2
3
4
5
6// id -> user
Map<Integer, User> userMap = users.stream()
.collect(Collectors.toMap(
User::getId,
user -> user
));groupingBy:一个 key 对一组 value。把同类数据装进同一个桶里。适合“分类分组”。1
2
3// department -> users
Map<String, List<User>> deptMap = users.stream()
.collect(Collectors.groupingBy(User::getDept));
groupingBy 和 downstream collector 配合
1 | [ |
groupingBy:按什么分组- downstream collector:每组最后变成什么
groupingBy1
Collectors.groupingBy(分组规则, 每组怎么处理)
- 普通
groupingBy1
2
3// 部门 -> 用户列表
Map<String, List<User>> map = users.stream()
.collect(Collectors.groupingBy(User::getDept));
作用:分组后统计每组数量groupingBy + counting()1
2
3
4
5
6// 部门 -> 人数
Map<String, Long> map = users.stream()
.collect(Collectors.groupingBy(
User::getDept,
Collectors.counting()
));1
2
3
4{
"技术部" = 2,
"销售部" = 1
} groupingBy + counting()1
2
3
4
5
6// 部门 -> 人数
Map<String, Long> map = users.stream()
.collect(Collectors.groupingBy(
User::getDept,
Collectors.counting()
));
作用:分组后,不保留整个对象,而是提取某个字段groupingBy + mapping()mapping:在每个分组内部,再做一次 map1
2
3
4
5
6// 部门 -> 名字列表
Map<String, List<String>> map = users.stream()
.collect(Collectors.groupingBy(
User::getDept,
Collectors.mapping(User::getName, Collectors.toList())
));1
2
3
4{
"技术部" = ["张三", "李四"],
"销售部" = ["王五"]
}groupingBy + mapping()1
2
3
4
5
6// 部门 -> 姓名列表
Map<String, List<String>> map = users.stream()
.collect(Collectors.groupingBy(
User::getDept,
Collectors.mapping(User::getName, Collectors.toList())
));
作用:分组后对某个数值字段求和groupingBy + summingInt / summingLong / summingDouble1
2
3
4
5
6// 部门 -> 工资总和
Map<String, Integer> map = users.stream()
.collect(Collectors.groupingBy(
User::getDept,
Collectors.summingInt(User::getSalary)
));groupingBy + summingInt()1
2
3
4
5
6// 部门 -> 工资总和
Map<String, Integer> map = users.stream()
.collect(Collectors.groupingBy(
User::getDept,
Collectors.summingInt(User::getSalary)
));
判断方法
你写 Stream 时,可以先问自己下面几个问题。
第一步:我要不要筛选数据?1
filter(...)
第二步:我要不要把元素转换成别的东西?1
map(...)
第三步:我要不要对每个元素执行动作?1
forEach(...)
第四步:最后我要什么结果?
| 目的 | 函数 |
|---|---|
要List |
collect(toList()) |
要Set |
collect(toSet()) |
要Map |
collect(toMap()) |
| 要数量 | count() |
| 要第一个 | findFirst() |
| 要真假判断 | anyMatch() / allMatch() / noneMatch() |










