使用Stream处理集合数据【Java 1.8 新特性】
使用 Stream
处理集合数据【Java 1.8 新特性】
Stream
是Java 8中引入的一个重要概念,它提供了对集合对象进行一系列操作的新方式,包括筛选、转换、聚合等。Stream API以声明式方式提供了对数据集合的高效操作,并且可以并行处理数据。
首先构建一个类,下面举例用得着
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Customer {
private String name; // 姓名
private Integer age; // 年龄
private Double score;// 积分
}
然后构建一个混乱的集合
ArrayList<Customer> customers = new ArrayList(){{
add(new Customer("user01",10,7000.0));
add(new Customer("user02",10,2000.0));
add(new Customer("user03",20,3000.0));
add(new Customer("user04",20,4000.0));
add(new Customer("user05",30,5000.0));
add(new Customer("user06",30,6000.0));
add(new Customer("user07",40,1000.0));
}};
中间操作
[!NOTE]
带有⭐ ⭐⭐ 的方法在平常开发中使用频率高
带有⭐ ⭐的方法在平常开发中使用频率中等
带有⭐ 的方法在平常开发中使用频率低
不带⭐ 的方法在平常开发中几乎不会用到
filter 过滤 ⭐⭐⭐
// 筛选出年龄大于20,积分大于1000 的用户并返回
List<Customer> collect = customers.stream()
.filter(c -> c.getAge() > 20 && c.getScore() > 1000 )
.collect(Collectors.toList());
对于 filter
,需掌握3个知识点
-
filter
方法可以多次调用,比如先按年龄过滤,再按积分过滤List<Customer> collect = customers.stream() .filter(c -> c.getAge() > 20) .filter(c -> c.getScore() > 1000 ) .collect(Collectors.toList());
[!CAUTION]
filter
的多次调用中,当前是在上一次过滤的基础上再过滤,举个例子:如果一条数据满足条件b但不满足条件a,而过滤顺序是条件a在前条件b在后,那么这条数据在条件a处就被过滤掉了,不会轮到条件b处理。
map 映射 ⭐ ⭐⭐
将流中的每个元素映射到另一个元素,这么说可能有点抽象,通俗来说就是操作集合中的对象。
// 将每个用户的积分+1
customers.stream().map(customer -> {
customer.setScore(customer.getScore()+1);
return customer;
}).collect(Collectors.toList());
结果如下
0 = {Customer@706} "Customer(name=user01, age=10, score=7001.0)"
1 = {Customer@707} "Customer(name=user02, age=10, score=2001.0)"
2 = {Customer@708} "Customer(name=user03, age=20, score=3001.0)"
3 = {Customer@709} "Customer(name=user06, age=20, score=4001.0)"
4 = {Customer@710} "Customer(name=user06, age=30, score=5001.0)"
5 = {Customer@711} "Customer(name=user06, age=30, score=6001.0)"
6 = {Customer@712} "Customer(name=user06, age=40, score=1001.0)"
对于 map
,需掌握3个知识点
map
方法可以多次调用,比如先处理年龄+1,再处理积分+1map
的lambda表达式有返回值,需要return,至于是否需要使用一个新集合作为变量接收结果,一般我不建议用,因为“新集合”是浅拷贝,修改了原数据,“新集合”中的数据也会跟着变- 结尾不用
.collect(Collectors.toList())
,操作是无效的~
flatMap 将流中的每个元素替换为目标元素的流
flatMap
用于将流中的每个元素(这些元素通常是集合或数组)转换成流,然后将这些流连接起来形成一个单一的流。如下代码
// 如果customer的年龄大于20,就将当前的customer对象复制2份,否则复制3份
List<Customer> collect = customers.stream().flatMap(customer -> {
return customer.getAge() > 20?
IntStream.range(0, 2).mapToObj(
i -> new Customer(customer.getName(), customer.getAge(), customer.getScore()))
: IntStream.range(0, 3).mapToObj(
i -> new Customer(customer.getName(), customer.getAge(), customer.getScore()));
}).collect(Collectors.toList());
对于 flatMap
,需掌握3个知识点
flatMap
方法可以多次调用- lambda表达式返回的对象必须是一个
Stream<U>
类型 - 结尾不用
.collect(Collectors.toList())
,操作是无效的~
[!TIP]
平时开发几乎用不到
flatMap
方法,处理List< List<Obj> >
型的数据用到过。
limit 截取 ⭐ ⭐
它用于截取流中的前 n
个元素,返回一个由原始流的前 n
个元素组成的新流。如果流中的元素少于 n
个,则返回包含所有元素的流
// 截取customers前两个元素
List<Customer> collect = customers.stream().limit(2).collect(Collectors.toList());
结果如下
0 = {Customer@707} "Customer(name=user01, age=10, score=7000.0)"
1 = {Customer@708} "Customer(name=user02, age=10, score=2000.0)"
对于 limit
,需掌握3个知识点
limit
方法可以多次调用(一般不会连着调多次)limit
是浅拷贝,修改了原数据,“新集合”中的数据也会跟着变- 结尾不用
.collect(Collectors.toList())
,操作是无效的~
sorted 排序 ⭐ ⭐⭐
/**
* 顺序排序(两种方案均可)
* */
List<Customer> collect = customers.stream()
.sorted(Comparator.comparingInt(Customer::getAge))
.collect(Collectors.toList());
/**
* 倒序排序,加 reversed 即可
* */
List<Customer> collect = customers.stream()
.sorted(Comparator.comparingInt(Customer::getAge).reversed())
.collect(Collectors.toList());
对于 sorted
,需掌握3个知识点
-
sorted
是浅拷贝,修改了原数据,“新集合”中的数据也会跟着变 -
结尾不用
.collect(Collectors.toList())
,操作是无效的~ -
sorted
方法可以多次调用,比如先按年龄排,再按积分排List<Customer> collect = customers.stream() .sorted(Comparator.comparingInt(Customer::getAge)) .sorted(Comparator.comparingDouble(Customer::getScore).reversed()) .collect(Collectors.toList());
[!CAUTION]
但是哈,这种排序会扰乱上一步排序的结果,比如上述代码,其实我希望是在年龄排序的基础上,再排序积分,结果如下,可以看出年龄排序已经被打乱了~,所以一般不会用到多次调用
sorted
0 = {Customer@734} "Customer(name=, age=10, score=7000.0)"
1 = {Customer@739} "Customer( age=30, score=6000.0)"
2 = {Customer@738} "Customer( age=30, score=5000.0)"
3 = {Customer@737} "Customer( age=20, score=4000.0)"
4 = {Customer@736} "Customer( age=20, score=3000.0)"
5 = {Customer@735} "Customer( age=10, score=2000.0)"
6 = {Customer@740} "Customer( age=40, score=1000.0)"既然如此,那么如何达到要求呢?当然是从
Comparator
上下手了,在第一个排序条件后跟.thenComparing...
,如下List<Customer> collect = customers.stream() .sorted( Comparator.comparingInt(Customer::getAge) .thenComparingDouble(Customer::getScore) ).collect(Collectors.toList());
排序结果如下,达到要求了~
0 = {Customer@737} "Customer( age=10, score=2000.0)"
1 = {Customer@738} "Customer( age=10, score=7000.0)"
2 = {Customer@739} "Customer( age=20, score=3000.0)"
3 = {Customer@740} "Customer( age=20, score=4000.0)"
4 = {Customer@741} "Customer( age=30, score=5000.0)"
5 = {Customer@742} "Customer( age=30, score=6000.0)"
6 = {Customer@743} "Customer( age=40, score=1000.0)"[!NOTE]
- thenComparing支持多种数据类型排序:int,long,double
- Comparator 也支持链式编程
mapToDouble / mapToInt / mapToLong 提取数字 ⭐⭐
用于将流中的每个元素映射到一个 double
值。这个操作通常用于将流中的元素转换成数值型数据,以便进行数值计算。这些 方法接受一个函数作为参数,这个函数会被应用到流中的每个元素上,并将每个元素转换成一个 数字 值。这个方法返回一个 DoubleStream
/ LongStream
/ IntStream
,可以被进一步用于数值操作,如求和、平均值计算等。
// 把customers中的各元素的积分值提取出来装入一个list
List<Double> collect = customers.stream().mapToDouble(Customer::getScore).boxed().collect(Collectors.toList());
// 计算积分总和
double totalScore = customers.stream().mapToDouble(Customer::getScore).sum();
// 获取最大积分
double maxScore = customers.stream().mapToDouble(Customer::getScore).max();
// 获取最小积分
double minScore = customers.stream().mapToDouble(Customer::getScore).min();
// 计算平均积分
double minScore = customers.stream().mapToDouble(Customer::getScore).average();
方法还有很多,感兴趣可以去探索,这里就不一一例举了~
对于 mapToDouble
/ mapToInt
/ mapToLong
,需掌握1个知识点
mapToDouble
/mapToInt
/mapToLong
可以链式编程,但是一般开发不这么做
终止操作
forEach:遍历流中元素⭐⭐⭐
customers.stream().forEach(
customer -> {
Double score = customer.getScore();
customer.setScore(score + 1);
}
);
对于 forEach
,需掌握2个知识点
forEach
中如果修改数据,影响原集合forEach
方法仅能一次调用
collect:将流转换成其他形式(如集合)⭐⭐⭐
需掌握 Collectors
工具类,它提供了多种收集方式
- Collectors.toList():将流收集到一个新的
List
中 ⭐⭐⭐ - Collectors.toSet():将流收集到一个新的
Set
中,这会去除重复元素 ⭐⭐⭐ - Collectors.toCollection():将流收集到给定的
Collection
中 - Collectors.joining():将流中的元素连接成一个字符串 ⭐
- Collectors.toMap():将流元素收集到一个
Map
中 ⭐⭐⭐ - Collectors.groupingBy():根据某个属性对流元素进行分组,结果收集到一个
Map
中⭐ - Collectors.reducing():通过某个连接动作(如加法)将所有元素汇总成一个汇总结果⭐
- Collectors.collectingAndThen():在另一个收集器的结果上执行映射操作
- 除了使用
Collectors
提供的静态方法,开发者还可以自定义收集器(我没这么玩过,感兴趣的可以研究一下)
对于 collect
,需掌握1个知识点
collect
方法仅能一次调用
reduce:通过某个连接动作将所有元素汇总成一个汇总结果 ⭐⭐
它用于通过某个连接动作将所有元素汇总成一个汇总结果。reduce
方法可以用于多种类型的归约操作,比如求和、求最大值、求最小值等。【做大数据项目时用的最多】
// 计算customers中各元素积分总和
double reduce = customers.stream().mapToDouble(i -> i.getScore()).reduce(0, (a, b) -> a + b);
// 计算customers中积分最大值
double reduce = customers.stream().mapToDouble(i -> i.getScore()).reduce(0, (a, b) -> a > b ? a:b);
// 计算customers中积分最小值
double reduce = customers.stream().mapToDouble(i -> i.getScore()).reduce(0, (a, b) -> a < b ? a:b);
对于 reduce
,需掌握1个知识点
reduce
方法仅能一次调用
allMatch、anyMatch、noneMatch:检查流中的元素是否与给定的条件匹配
// 是否所有customers中元素的积分数值都大于10
boolean b = customers.stream().allMatch(i -> i.getScore() > 10);
// customers中是否存在有的元素的积分数值大于10
boolean b = customers.stream().anyMatch(i -> i.getScore() > 10);
// customers中所有的元素的积分数值都不大于10
boolean b = customers.stream().noneMatch(i -> i.getScore() > 10);
对于 allMatch
、anyMatch
、noneMatch
,需掌握1个知识点
allMatch
、anyMatch
、noneMatch
方法仅能一次调用
count:返回流中元素的数量 ⭐
// 统计customers中年龄大于20的数据量
long count = customers.stream().filter( i -> i.getAge() > 20 ).count();
对于 count
,需掌握1个知识点
1. `count` 方法仅能一次调用
findFirst、findAny:返回流中的第一个或任意一个元素 ⭐
Customer any = customers.stream().findAny().get();
Customer first = customers.stream().findFirst().get();
对于 findFirst
、findAny
,需掌握3个知识点
findFirst
、findAny
方法仅能一次调用Optional
类的orElse
方法允许你提供一个默认值,如果Optional
为空,则返回这个默认值- orElseGet:类似于
orElse
,但接受一个Supplier
函数,当Optional
为空时,会调用这个函数来生成默认值
拓展: parallelStream
的使用
parallelStream 是 Java 8 引入的 Stream API 的一部分,它允许开发者并行处理集合中的元素。当在集合上调用 parallelStream() 方法时,会得到一个并行流,这个流可以利用多个CPU核心加速处理过程。相较于 Stream 有如下优势
-
方法和 Stream 相差无几,上述的方法也可以在
parallelStream
中使用,举俩例子如下List<Customer> integerStream = arrayList.parallelStream().filter( i -> i.getAge() > 10 ).collect(Collectors.toList()); arrayList.parallelStream().filter( i -> i.getAge() > 10 ).forEach(i -> System.out.println(i));
-
并行处理能力:parallelStream 利用 Java 的 ForkJoinPool 线程池来并行处理数据,这意味着它可以同时在多个 CPU 核心上执行任务,从而加速处理过程。
-
性能提升:对于计算密集型任务,parallelStream 可以显著减少总体执行时间,因为它允许多个处理器核心同时工作。
-
更好的资源利用:在多核处理器系统上,parallelStream 可以更有效地利用所有可用的处理器核心,而顺序流只能在单个线程上执行。
-
优化的算法实现:某些算法在并行执行时更加高效,因为它们可以被分解成多个独立的子任务,每个任务可以在不同的处理器核心上并行执行。
-
处理大数据集:对于大规模数据集,parallelStream 可以提供更快的处理速度,因为它避免了单个线程处理所有数据的瓶颈。
然而,parallelStream 也有一些局限性和需要注意的地方:
-
线程安全:在 parallelStream 中执行的操作必须是线程安全的,因为多个线程可能会同时执行这些操作。
-
顺序不确定性:由于并行执行,操作的顺序是不可预测的,这可能会影响到那些依赖于元素顺序的操作。
-
额外的开销:并行流引入了额外的线程管理和任务调度开销,对于小数据集或简单的操作,这可能会导致性能下降。
-
调试难度:并行流可能会使调试变得更加困难,因为多个线程的交互可能导致难以追踪的错误。
-
I/O 绑定操作:对于 I/O 绑定的操作(如文件读写、网络请求等),并行流可能不会带来性能提升,因为这些操作的速度受限于 I/O 速度,而不是 CPU 处理能力
可以这样理解:parallelStream
运用了多线程,在处理大量数据的场景下的速度比 Stream 明显高;可也就是因为多线程,一些顺序性操作是不可预测的。
先记录到这里,后续有总结会持续补上
本文来自博客园,作者:勤匠,转载请注明原文链接:https://www.cnblogs.com/JarryShu/articles/18529106
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现