目录
前言
Stream流 是什么?
为什么要用Steam流
常见stream流使用案例
映射 map() & 集合 collect()
单字段映射
多字段映射
映射为其他的对象
映射为 Map
去重 distinct()
过滤 filter()
Stream流的其他方法
使用Stream流的弊端
前言
当你某天看到舍友的代码不再写for循环时,你的反应:
你还在 new Collection<>() ,写着for循环的时候,舍友已经开始偷偷卷你,更改代码风格了
本文将带着大家简单理解 Stream 流,并通过部分案例描述 Stream 流 的实用方法
Stream流 是什么?
Stream 流是 Java 8 引入的一个强大工具,它提供了一种全新的方式来处理集合和数组等数据源,使得数据处理变得更加简单、高效和易于理解。
通俗的理解起来就是提供了一种更加便利的遍历处理方式。
如果你要问我 Stream流用起来什么感觉?
那我只能说,,这种感觉就像飞翔在~~
噢不对,,感觉就是:
为什么要用Steam流
Stream 流的主要用途是提供一种高效且表达力高的方式来处理集合和数组等数据源。通过使用 Stream 流,可以避免显式的迭代器和循环,使得代码更加简洁、易读。Stream 流支持复杂的查询/过滤、映射/转换、归约/汇总等操作,能够极大地简化数据处理的复杂度。
总结起来还是:简洁、易读
当然,这也让你的代码看起来更高级那么一点点~~
如下案例,拿到所有的评论的id 集合的两种方法。
- 第一种-for循环便利获取
List<Comment> list = commentMapper.selectList(wrapper);
List<Integer> commentId = new ArrayList<>();
for(Comment c : list){
commentId.add(c.getId());
}
- 第二种-Stream流获取
List<Comment> list = commentMapper.selectList(wrapper);
List<Integer> commentId = list.stream().map(Comment::getId).collect(Collectors.toList());
两种方法的区别显而易见
下面介绍stream流比较实用的方法
常见stream流使用案例
在这里我们准备一个简单的对象来进行案例测试,只约定两个字段。
@Data
@AllArgsConstructor
public class StreamTestObject {
Integer id1;
Integer id2;
}
映射 map() & 集合 collect()
map() 方法是最常用的方法之一,它可以将流中的每个元素转换成另一种形式,返回转换后的 Stream。
如前文的例子所示,
单字段映射
List<StreamTestObject> streamTestObjects = Arrays.asList(
new StreamTestObject(1, 2),
new StreamTestObject(3, 4),
new StreamTestObject(5, 6));
// 便于观察变化
Stream<StreamTestObject> stream = streamTestObjects.stream();
Stream<Integer> id1Stream = streamTestObjects.stream().map(StreamTestObject::getId1);
看代码我们可以看到,map方法将对象的stream流映射为了其中 id1 这个字段的stream流
拿到这个字段的流后,可以做些什么呢?
最常用的方法之一就是与 集合 collect() 搭配起来使用。
那么 collect() 方法能做写什么呢?
- 用途:将流中的元素累积成一个汇总结果,我们可以按照自己的需求将结果汇总为一个 List、Set、Map 等
如下代码所示
List<StreamTestObject> streamTestObjects = Arrays.asList(
new StreamTestObject(1, 2),
new StreamTestObject(3, 4),
new StreamTestObject(5, 6));
Stream<StreamTestObject> stream = streamTestObjects.stream();
Stream<Integer> id1Stream = streamTestObjects.stream().map(StreamTestObject::getId1);
List<Integer> collectList = id1Stream.collect(Collectors.toList());
// Set<Integer> collectSet = id1Stream.collect(Collectors.toSet());
// 连起来使用一行代码可以写成这样
collectList = streamTestObjects.stream().map(StreamTestObject::getId1).collect(Collectors.toList());
// collectSet = streamTestObjects.stream().map(StreamTestObject::getId1).collect(Collectors.toSet());
System.out.println("collectList:" + collectList);
// 输出结果 collectList:[1, 3, 5]
结果能够把 id1 成功收集起来,代码的易读性也体现在其中。我们一眼就能看出这行代码映射了id1 这个字段为一个 List 或 Set。
多字段映射
那如果我们想要对象集合中的 id1 和 id2 都汇总到一个 List<Integer> 集合里,应该如何操作呢。
这里我们可以使用一个 flatMap() 方法
- 用途:将流中的每个元素都转换成另一个流,然后将所有流连接成一个流。
直接上代码
List<StreamTestObject> streamTestObjects = Arrays.asList(
new StreamTestObject(1, 2),
new StreamTestObject(3, 4),
new StreamTestObject(5, 6));
List<Integer> collectList = streamTestObjects.stream()
.flatMap(object ->Stream.of(object.getId1(), object.getId2())).collect(Collectors.toList());
System.out.println("collectList:" + collectList);
//输出结果 collectList:[1, 2, 3, 4, 5, 6]
在这个例子中,Stream.of(obj.getId1(), obj.getId2())为每个对象生成了一个包含两个ID的流,它在map中 形成了一个临时的流
然后flatMap将这些流“展平”成了一个包含所有ID的流,最后我们通过collect(Collectors.toList())将这个流收集到了一个列表中。
映射为其他的对象
有的时候的业务需求需要我们把一个对象集合转化为另外一个集合对象,如果是单纯的 字段copy,我们可以使用 BeanUtils 或者 MapStruct 等方法实现。
如果转化的过程中设计业务逻辑,那么就需要 Stream流出手了。
这里需要设计到一个显示 return 的写法,上代码,先准备一个另外的对象,只包含一个id字段
@Data
@NoArgsConstructor
@AllArgsConstructor
public class StreamOtherObject {
Integer id;
}
然后 我们将上述测试对象的id1,转化为这里的id字段。
List<StreamTestObject> streamTestObjects = Arrays.asList(
new StreamTestObject(1, 2),
new StreamTestObject(3, 4),
new StreamTestObject(4, 5));
List<StreamOtherObject> collectList = streamTestObjects.stream()
.map(streamTestObject -> {
StreamOtherObject object = new StreamOtherObject();
object.setId(streamTestObject.getId1());
return object;
}).collect(Collectors.toList());
System.out.println("collectList:" + collectList);
//输出结果 collectList:[StreamOtherObject(id=1), StreamOtherObject(id=3), StreamOtherObject(id=4)]
注意看返回的集合对象已经是我在表达式里return 的 StreamOtherObject了。
也就是 return 的内容就是集合的具体对象
映射为 Map
Stream流还能把集合映射为一个Map,这里我们测试用例为将映射结果设置为 key 为 id1, value 为对象本身
List<StreamTestObject> streamTestObjects = Arrays.asList(
new StreamTestObject(1, 2),
new StreamTestObject(3, 4));
Map<Integer, StreamTestObject> map = streamTestObjects.stream()
.collect(Collectors.toMap(
StreamTestObject::getId1,
Function.identity(),
(existsOne, replaceOne) -> replaceOne));
System.out.println("collectMap:" + map);
//输出结果 collectMap:{1=StreamTestObject(id1=1, id2=2), 3=StreamTestObject(id1=3, id2=4)}
可以看到,toMap() 方法中,传递了3个参数,前两个分别为 key , value
第三个参数传了一个表达式,这里的逻辑表示如果发生冲突,就保留 Map中新的那个对象,而不是保留它。同时,第三个参数也处理了冲突,如果你没有对于 key 相同的情况做处理,也就是 key 冲突了,方法将抛出一个IllegalStateException。
所以你需要做对应的处理,如try catch 下来,或者进行冲突处理,即传递第三个参数。
去重 distinct()
对于拿到的流结果,我们有的时候有去重的需求,当然我们可以转为 toSet() 进行去重
stream流同样提供了一个方法进行去重,就是 distinct() 方法
- 用途:去除流中的重复元素,返回包含不同元素的 Stream。
这里比较好理解,我们直接看案例
List<StreamTestObject> streamTestObjects = Arrays.asList(
new StreamTestObject(1, 2),
new StreamTestObject(3, 4),
new StreamTestObject(4, 5));
List<Integer> collectList = streamTestObjects.stream()
.flatMap(object ->Stream.of(object.getId1(), object.getId2())).distinct().collect(Collectors.toList());
System.out.println("collectList:" + collectList);
//输出结果 collectList:[1, 2, 3, 4, 5]
过滤 filter()
- 用途:根据提供的条件过滤元素,返回满足条件的 Stream。
过滤的方法也是比较常用的方法,也是比较多业务中有这个需求的。这里介绍两种方法
在拿到一个流后,也许不是所有的元素我们都需要。我们需要保存满足特定条件的元素,这时候就可以使用 filter方法来实现。
这里的案例表示筛选除 id1 为 1,id2 为 4 的数据,代码如下:
List<StreamTestObject> streamTestObjects = Arrays.asList(
new StreamTestObject(1, 2),
new StreamTestObject(3, 4),
new StreamTestObject(4, 5));
List<StreamTestObject> collectList = streamTestObjects.stream()
.filter(object -> object.getId1().equals(1) || object.getId2().equals(4))
.collect(Collectors.toList());
System.out.println("collectList:" + collectList);
//输出结果 collectList:[StreamTestObject(id1=1, id2=2), StreamTestObject(id1=3, id2=4)]
如果你的过滤逻辑比较复杂可以使用显示 return 写法来过滤
List<StreamTestObject> streamTestObjects = Arrays.asList(
new StreamTestObject(1, 2),
new StreamTestObject(3, 4),
new StreamTestObject(4, 5));
List<StreamTestObject> collectList = streamTestObjects.stream()
.filter(object -> {
int id1 = 1;
int id2 = 4;
return object.getId1().equals(id1) || object.getId2().equals(id2);
}).collect(Collectors.toList());
System.out.println("collectList:" + collectList);
//输出结果 collectList:[StreamTestObject(id1=1, id2=2), StreamTestObject(id1=3, id2=4)]
在代码块里可以编辑自己自定义的过滤逻辑
这里要注意返回值是一个布尔值,如果为 true,则保留这项数据,不满足,则进行一项数据处理。
Stream流的其他方法
前文是 Stream 流比较常见的方法案例,它还提供了很多其他的接口来实现对应的场景,如:
- sorted()
- 用途:对流中的元素进行自然排序(需实现 Comparable 接口),返回排序后的 Stream。
- 示例:对用户列表按年龄进行排序。
- limit(long maxSize)
- 用途:截断流,使其包含不超过给定数量的元素,返回截断后的 Stream。
- 示例:只取用户列表中的前三个用户。
- skip(long n)
- 用途:跳过流中的前 n 个元素,返回剩下的元素的 Stream。
- 示例:跳过用户列表中的前两个用户,取后面的用户。
- forEach(Consumer<? super T> action)
- 用途:这是大家比较熟悉的操作,在代码编写中可以省去 .Stream() 的写法。意为对流中的每个元素执行提供的操作,这是一个终结操作。
- 示例:遍历用户列表并打印每个用户的名字。
使用Stream流的弊端
学习了Stream流 的优点之后,也需要知道随之产生的弊端有短些,这里我列举几个主要的内容
- 性能问题:
- 多次遍历:有时为了完成一个操作,可能需要多次遍历数据源。例如,先过滤(filter)再映射(map)最后收集(collect),这会导致数据被多次遍历。
- 并行流开销:虽然并行流可以加速处理过程,但它们引入了额外的线程管理开销,并且不总是能带来性能提升,尤其是在数据源较小或操作相对简单时。
- 懒加载导致的意外行为:Stream操作是懒加载的,这意味着它们直到需要结果时才会执行。这可能导致在调试时难以追踪问题的源头,或者在某些情况下,当流操作依赖于外部状态时,可能导致不可预测的行为。
- 可读性和维护性:
??:前面不是可读性强吗,怎么有问题了?如果嵌套太多层的操作方法,也会使得表达式的可读性降低
- 复杂逻辑难以追踪:对于包含多个复杂操作(如多重过滤、映射、归约等)的Stream链,其逻辑可能变得难以理解和追踪。
- 调试困难:由于Stream操作的延迟执行和中间操作的无状态性,调试Stream代码可能会比传统循环更加困难。
- 错误处理:
- 异常处理复杂:在Stream操作中处理异常(如尝试映射一个可能抛出异常的函数)比在传统循环中更复杂。Stream API没有直接支持异常处理机制,通常需要通过try-catch块或自定义函数来处理。
- 内存消耗:
- 中间结果存储:Stream API在执行过程中可能会创建中间结果的临时集合,尤其是在进行复杂操作时,这可能会增加内存消耗。
到这里,同学们可以多实操一下这些方法来巩固知识。文章如有遗漏或建议更改的部分欢迎佬们指出。