前言
做后端开发,集合处理几乎是每天都要面对的工作。最开始写业务,遇到列表过滤、对象转换、分组统计,第一反应就是写 for 循环,里面嵌套 if 判断,中间定义各种临时集合接收结果。业务逻辑复杂一点,循环嵌套三四层,代码几十行,后续接手维护的时候,读起来非常费劲,一不小心还会引入 bug。
很多人知道 stream 可以简化集合遍历,但大部分人仅仅停留在filter、map基础用法上,遇到分组、归约、扁平化处理还是退回 for 循环。还有一部分同学盲目滥用 stream,线上出现 key 重复报错、并行流线程安全问题,反而制造更多隐患。
stream 不是简单把 for 循环换个写法,它是一套流式数据处理模型,中间操作惰性执行,一次遍历完成多步处理。下面结合真实业务场景,从基础过渡到高级用法,同时贴出对比代码,方便直接复制到项目使用。
传统循环臃肿写法
模拟业务场景:订单列表,过滤有效订单,提取订单编号,按用户 id 分组统计订单金额。先看普通 for 循环实现:
import java.math.bigdecimal;
import java.util.arraylist;
import java.util.hashmap;
import java.util.list;
import java.util.map;
class order {
private long userid;
private string orderno;
private bigdecimal amount;
private integer status; // 1有效 0无效
public order(long userid, string orderno, bigdecimal amount, integer status) {
this.userid = userid;
this.orderno = orderno;
this.amount = amount;
this.status = status;
}
// getter省略
public long getuserid() {return userid;}
public string getorderno() {return orderno;}
public bigdecimal getamount() {return amount;}
public integer getstatus() {return status;}
}
public class fordemo {
public static void main(string[] args) {
list<order> orderlist = new arraylist<>();
// 模拟数据
orderlist.add(new order(1l,"o001",new bigdecimal("299"),1));
orderlist.add(new order(1l,"o002",new bigdecimal("599"),1));
orderlist.add(new order(2l,"o003",new bigdecimal("199"),0));
map<long,bigdecimal> usertotalmap = new hashmap<>();
list<string> validordernos = new arraylist<>();
for(order order : orderlist){
// 过滤无效订单
if(order.getstatus()!=1){
continue;
}
validordernos.add(order.getorderno());
// 分组累加金额
bigdecimal total = usertotalmap.getordefault(order.getuserid(),bigdecimal.zero);
usertotalmap.put(order.getuserid(),total.add(order.getamount()));
}
system.out.println("有效订单号:"+validordernos);
system.out.println("用户总金额:"+usertotalmap);
}
}
上面代码逻辑没问题,但可以看到需要手动维护两个容器,写 if 判断,变量多,业务再复杂一点,代码会持续膨胀。
stream 高级实战改造
同样的业务逻辑,使用 stream 一次性完成过滤、提取、分组聚合。这里用到collectors.groupingby结合collectors.reducing做金额归约求和,这也是项目里高频的高级用法csdn博...。
import java.math.bigdecimal;
import java.util.*;
import java.util.stream.collectors;
public class streamadvdemo {
public static void main(string[] args) {
list<order> orderlist = new arraylist<>();
orderlist.add(new order(1l,"o001",new bigdecimal("299"),1));
orderlist.add(new order(1l,"o002",new bigdecimal("599"),1));
orderlist.add(new order(2l,"o003",new bigdecimal("199"),0));
// 过滤有效订单
list<order> validorder = orderlist.stream()
.filter(o -> o.getstatus() == 1)
.collect(collectors.tolist());
// 获取订单编号列表
list<string> ordernolist = validorder.stream()
.map(order::getorderno)
.collect(collectors.tolist());
// 按用户id分组,求和订单金额
map<long, bigdecimal> useramountsum = validorder.stream()
.collect(collectors.groupingby(
order::getuserid,
collectors.reducing(
bigdecimal.zero,
order::getamount,
bigdecimal::add
)
));
system.out.println(ordernolist);
system.out.println(useramountsum);
}
}
代码没有各种临时集合,逻辑和业务语义一一对应,读代码就是读需求。
flatmap 扁平化处理
实际开发经常遇到集合嵌套,比如一个用户有多条收货地址,list<user>里面每个 user 持有 list<address>,想要把全部地址提取成一个扁平列表。很多同学两层 for 循环嵌套,flatmap 专门解决嵌套集合拆解问题csdn博...。
class user{
private list<string> addresslist;
public user(list<string> addresslist){
this.addresslist = addresslist;
}
public list<string> getaddresslist() {return addresslist;}
}
// flatmap示例
list<user> userlist = arrays.aslist(
new user(arrays.aslist("北京朝阳","北京海淀")),
new user(arrays.aslist("上海浦东"))
);
list<string> alladdress = userlist.stream()
.flatmap(user -> user.getaddresslist().stream())
.collect(collectors.tolist());
system.out.println(alladdress);
partitioningby 分区
partitioningby是特殊分组,传入布尔条件,自动分成 true、false 两组,适合把集合切分成符合条件、不符合条件两部分,比如区分大额订单和普通订单csdn博...。
map<boolean, list<order>> partmap = validorder.stream()
.collect(collectors.partitioningby(o -> o.getamount().compareto(new bigdecimal("300"))>0));
// true是大于300订单,false小于等于300
list<order> bigorder = partmap.get(true);
list<order> smallorder = partmap.get(false);
stream 开发高频踩坑点
写代码爽是一方面,线上踩坑才是重点,我项目里遇到过不少 stream 引发的 bug,这里整理几个高频问题。
- collectors.tomap key 重复异常 当集合存在重复 key,直接
collectors.tomap()会抛出illegalstateexception。业务中一定要指定合并策略,不要直接裸用 tomap。
// 错误写法,key重复直接报错 // .collect(collectors.tomap(order::getuserid,order::getorderno)); // 正确,重复key保留后者 .collect(collectors.tomap(order::getuserid,order::getorderno,(oldval,newval)->newval));
- 流只能消费一次 流执行 collect 之后就关闭,不能二次复用,重复调用会抛出异常。需要多次处理,重新从原始集合生成 streamcsdn博...。
//错误 stream<order> stream = orderlist.stream().filter(o->o.getstatus()==1); stream.collect(collectors.tolist()); //stream再次调用会报错
不要盲目上 parallelstream 并行流 很多人看到并行就觉得性能高,直接所有地方加上 parallel。小数据量并行反而更慢,并行流会使用 forkjoinpool,操作非线程安全集合会出现数据错乱。业务没有明确大数据量计算需求,优先用普通 stream 串行流。
不要在 foreach 里面做复杂业务聚合 不少同学喜欢用 stream.foreach 循环里面 add 到外部 list,这种写法失去流的意义,并行场景还会并发异常,尽量使用 collect 收集结果。
stream 和 for 循环怎么选
不是所有场景都要用 stream,不要为了炫技强行改造。
- 业务集合过滤、转换、分组、统计,优先 stream,代码简洁可读性高;
- 集合元素很少,小于千条级别,简单循环操作,普通 for 循环性能开销更小;
- 需要多层复杂业务逻辑、大量变量中间计算,硬写 stream 会导致 lambda 嵌套很深,反而可读性变差,老老实实写循环。
写在最后
stream 的核心价值,是把业务逻辑和循环控制解耦,我们只需要关心要做什么,而不是怎么循环遍历。很多人觉得 stream 难懂,本质是对 collector 收集器体系不熟悉,多把分组、归约、分区这些高级 api 用到实际项目,慢慢就能摆脱到处写 for 循环的局面。但是也要记住,stream 不是银弹,合理取舍,写出可读性优先的业务代码。
到此这篇关于java stream流高级用法的文章就介绍到这了,更多相关java stream流高级用法内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论