WorkProj
本文introduce 对于Stream的使用
在关系型数据库中处理数据还是很easy的,使用group by , order by等关键字可以方便查出各种数据
但是很多场景下数据不是在数据库中处理, 而是直接拎到内存中处理, 这个时候Stream就可以发挥巨大作用【相较于传统的for遍历】
比如java的dynamicSqlBuilder, 动态SQL查询出动态数据库表中的数据【所有都是动态的】, 这里的返回值就是List
要对List
流stream是支持数据处理操作的源生成的元素序列, 源可以是数组、文件、集合、函数; Stream不是集合,不是数据结构,主要目的就是计算
Stream创建方式一共有5种, 集合、数组、of值、文件、函数iterator无限流
Stream stream = distinctMap.values().stream
除此之外,也可以通过数组Arrays.sttream(数组)、 Stream.of(值1, 值2…)、Files.lines(文件)、 Stream.iterator(xxx)无限流 | Stream.generator(xxx)无限流
流Stream的操作分为两种:
打开流做出某种程度的映射
, 操作是惰性化的,没有真正开始流的遍历, 常见的map、filter都是进行流的遍历
,操作执行后,流就关闭了,不能再操作,因此一个流只能遍历一次,如collect、count中间操作可以为 0 ~ 多个, 一定程度的映射和操作
比如List list = Arrays.asList(1,1,2,3,4,5,6);
list.stream().filter(item -> item > 3)
//这里的item代表的就是流中的每一个元素,可以使用Lambda表达式
//4,5,6
list.stream().distinct()
//结果1,2,3,4,5,6
这个和数据库limit类似
list.stream().limit(3)
//1,1,2
和limit相反,跳过前X个元素
list.stream().skip(3)
//3,4,5,6
操作位置可以使用Lambda或者函数引用
list.stream().map(x -> x + 1)
//2,2,3,4,5,6,7
这个和Map的不同主要就是针对List>的情况, 一层流只能得到List, 要想直接得到最内层对象,就只能flatMap, 会将所有的List全转化为流
List> strList = [[“java,python”], [“cfeng”,“cshen”]]
strList.stream()
.flatMap(item -> item.Stream())
这个sorted中可以给出字段,就是按照默认的顺序排序,加上.reversed()
就可以倒序,当然也可以使用Lambda表达式进行定制的排序
list.stream.sorted(Comparator.comparing(Book:: getPublishTime).reversed())
终端操作只能一个, 会对流进行遍历(和requestBody一样,只能一次性)
if(list.stream.allMatch(item -> item > 3))
//这里false,因为1,1,2,3几个元素不满足
if(list.stream.anyMatch(item -> item > 5))
//true,因为6这个元素满足
if(list.stream.noneMatch(item -> item > 5))
//false,因为6这个元素满足
list.stream().count()
//个数7
list.stream().findFirst()
//1
list.stream().findAny()
//4 , 随机的
list.stream().min(Integer::CompareTo)
list.stream().min()
//1
list.stream().sum() //可以用lambda统计元素, 比如str.length求和
//1 + 1 + 2 + 3 + 4 + 5 + 6
list.stream().foreach(System::println);
//1 /n 1 .....
(T,T) -> T, 不断reduce, 两两比较; 其实max、min、sum都是reduce的特例,一个是两两比大小,一个是两两求和
其中的操作可以用迭代的思想看就是 一次迭代【通用计算方式】, 之后程序内部就会按照公式不断两两操作得到最后的结果
reduce可以实现从Stream中生成一个值,不是随意的,根据指定的模型
reduce(BinaryOperator 参数列表为一个函数式接口 可以看到就是接受T, U,之后返回R, 默认实现为minBy和maxBy就是取stream的最值,最要代码就是(a,b) -> comparator.compare(a, b) >= 0 ? a : b; 返回一个值public interface BinaryOperator<T> extends BiFunction<T,T,T> {
public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
}
public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
}
}
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);//接收两个参数 t 和 u, 返回 R
}
//比如实现求和操作, 这里return的就是求和之后的m1
reduce(m1, m2 -> m1 += m2)
reduce(T identity, BinaryOprator accumulator) 和上面相比,可以接受一个初始值, 返回的对象的初始值, 这里就是m1为100
//比如实现在100的基础上,再加上stream中的所有sum
//这里100的类型和 return的m1类型相同
reduce(100, (m1, m2) -> m1 += m2)
reduce(U identity, BiFunction accumulator, BinaryOpretor
第一个identity代表的返回对象的初始值, accumulator是reduce的计算逻辑, 增加的是一个参数组合器combiner
Stream支持并发操作,为了线程安全,对于reduce操作,每一个线程都会得到不同的result,这个参数的类型必须为返回的数据的类型
identity: 组合函数的处置, 累加器(操作器)的返回结果的初始值
累加器(操作器): 一个关联的、无状态函数, 可以将额外的结果合并到结果中
组合器: 组合两个值的关联、必须和累加器兼容, 只有并行流中才会执行, 也就是普通的stream()不会执行, 只有parallelStream()才会执行
最常见的collect就是Collectors.toList()策略, 就是将流转为一个List
Collectors.toList();
Collectors.toMap();
Collectors.toSet();
Collectors.toCollection();//含参构造,转化为new的容器
Collectors.toConcurrentMap();
分别将流转为List、Map、Set、Collection、 ConcurrentMap
连接符号可以是delimiter连接符; 或者delimiter连接符、prefix开始符,suffix结束符; 除此之外,还可以空参,代表delimiter为空格
Collectors.joining(); // 以空格连接
Collectors.joining("-"); //以-连接
Collectors.joining("【",",","】"); //以【x, y, z.....】
结果操作就是将collect结果再执行
List<String> list = Arrays.asList("cfeng", "java", "数字大屏");
String str = list.stream().collect(Collectors.collectingAndThen(Collectors.joining(",","[","]"), s -> s + "你好")); System.out.println(str);
和SQL的group by类似,内存分组可以减少压力
比如
// 按照字符串长度进行分组 符合条件的元素将组成一个 List 映射到以条件长度为key 的 Map> 中
servers.stream.collect(Collectors.groupingBy(String::length))
为了保证线程安全,可以采用安全的Map
Supplier<Map<Integer, Set<String>>> mapSupplier = () -> Collections.synchronizedMap(new HashMap<>());
Map<Integer, Set<String>> collect = servers.stream.collect(Collectors.groupingBy(String::length, mapSupplier, Collectors.toSet()));
或者可以直接使用groupingByConcurrent
list.stream().count() 效果一样
list.sream().collect(Collectors.counting())
List<String> names = students.stream().collect(Collectors.mapping(Student::getName, Collectors.toList()));
大小元素的操作,和list.stream.max终端操作一样
list.stream().collect(Collectors.minBy(Comparator.comparingInt(String.length))) //字符串中比较按照长度比较
list.stream().min(Comparator.comparingInt(String.length))
累加操作,和sum()终端操作一样
list.stream().collect(Collectors.summingInt(s -> s.length))) //字符串中比较按照长度比较
list.stream().min(Comparator.comparingInt(String.length))
summarizing对应的就是统计量,会将总数、总和、max、min、avg等统计量提取出来放到IntSummaryStatisics
(Double、 Long) 等统计对象中
IntSummaryStatistics statistics = list.stream().collect(Collectors.summarizingInt(s -> s.length)));
需要的数据再从对象中直接取出即可
这个方法对用的终端操作也就是reduce
其参数为BinaryOperator< T》 , 给两个相同类型的量,返回一个同类型的结果, (T, T) -> T, 默认实现为maxBy和minBy, 也就是返回最大/小值, 元素两两比较根据给定的策略淘汰一个, 随着迭代的进行,元素reduce
下面是一个例子
//统计城市个子最高的人
Comparator<Person> byHeight = Comparator.comparing(Person::getHeight); //子当以比较器为比较Person的身高
Map<String, Optional<Person>> tallestByCity = people.stream()
.collect(Collectors.groupingBy(Person::getCity, Collectors.reducing(BinaryOperator.maxBy(byHeight))));
首先cfeng想表明的stream确实便捷,同时使用stream不是单纯为了炫技,而是可以简练代码,增强可读性, 数据量比较小的情况下,iterator的效率高于stream; 但是在大数据量的情况下,还是stream的性能好
通过上面的操作可以看出,Stream的操作和SQL很相似,所以Stream主要就是在内存中处理数据【像SQL一样处理】
在Cfeng的work过程中,有客户数据库表的概念,也就是数据库表作为用户的数据,由用户进行相关的SQL操作,由于客户数据库表不是server端直接控制, 所以与其相关的数据处理就和传统的数据库表SQL操作有所不同。
//由server控制的数据库表直接使用SQL进行操作即可
//用户控制的数据库表 用户的界面是可视化的,并且用户不懂SQL,所以传入的只是条件,就像相关数据库工具一样
因此程序中可以dynamic的根据用户条件StringBuilder一个SQL出来
之后利用jdbcTemplate的queryList等方法执行SQL即可
但是因为直接使用的是jdbcTemplate查询出来的数据,所以一个视图对应的是一个List
那么这里处理这种List, 比如进行Group、Order相关SQL操作,使用Stream就很好处理了
现在比如我给一个模糊化的需求: 对于一个数据库表【商城名称、商城账户、成交时间、回执单数、交易总数、更新日期】,其中可能有数据【商城名称、商城账户、成交时间】相同,但是更新日期不同,认为这种数据为重复数据,需要取更新日期最新的数据,处理之后再按照商城名称进行分组求回执单数和交易总数的和
可以看到这里涉及三个操作: 去重 -> Group -> Sum, 去重可以认为是算法,Group、sum就是内存中进行SQL操作,Stream发挥重要作用
其实这就是一个算法题, 首先给一个List 这里给一下出现的数据的封装对象 简单容易思考的解法: 设置一个map的过滤器,让数据依次通过这个过滤器,去重之后,再进行group和相关SQL操作(全部都不使用Stream) 可以看到过程非常麻烦,后面的聚合Gather也是使用的手动聚合,flag标志过多,可读性和可维护性差 首先就是去重过程,可以看到使用一个Map作为filter的方式可读性很差,我们可以将primaryFields字段拼接为String, 以string为Key, 对应的Map为value进行去重,因为数据按照定义的deduplication字段有序,所以只需要取第一个出现的作为value 这里测试的时候,首先需要mock数据 之后就是对数据进行去重 去重之后对结果进行分组求和,group by, sum() 最后得到的结果如下 可以看到使用Stream因为可以调用现成的api,所以减少了很多工作量, 只是这个逻辑中需要注意几个地方:public class ParamObj {
List<String> primaryFields; //主码
String deduplicatedField; //去重key
String groupField; //group字段
List<String> sumField; //sum字段
SqlParam assetParams; //拼接SQL的相关字段
class SqlParam {
public static final Order CREATE_TIME_ORDER = new Order("create_time", Sort.Direction.DESC); //按照create_time降序Order
//客户数据库表名称
private String name;
//分页页号
private Integer pageNo;
//分页页面大小
private Integer pageSize;
//分组字段
private List<String> groupByFields;
//聚合对象(聚合字段以及聚合方式)
private List<GatherField> quotas;
//过滤对象(过滤字段和方式 <= >= ...)
private List<FieldFilter> filters;
//排序对象(排序字段和方式)
private List<Order> orders;
}
}
class GatherField {
String field; //字段名称
String alias; //字段别名, 在where之前的自定义视图字段名
GatherType type; //GatherType(enum); NO,AVG,SUM,MAX, MIN,COUNT
//和field组合可以成为sum(field)
}
public List<Map<String, Object>> deduplicateMapData(ParamObj paramObj) {
/**
* 拼接相关字段进行Sql的build
**/
//... 这里都是获取data所做的处理,比如分组group字段加上update_time,Order使用time
/**
* 这里就是抽象的利用jdbcTemplate获取客户数据库表的数据(有重复)
**/
List<Map<String, Object>> resp = getMapData(paramObj);
Map<String, Object> mapFilter = new HashMap<>();
Map<String, Object> firstRecord = resp.get(0);
//初始化filter
for(String s : originDistinctFields) {
mapFilter.put(s, firstRecord.get(s));
}
//遍历所需参数
Map<String, Object> recordMap;
Set<Map.Entry<String, Object>> filter;
boolean needDelete;
for(int i = 1; i < resp.size(); i ++) {
filter = mapFilter.entrySet();
needDelete = true;
recordMap = resp.get(i);
//对于每一行记录,只需要取第一个,因为group by会默认order, update_time为最大的时间
for(Map.Entry<String, Object> entry : filter) {
if(!recordMap.get(entry.getKey()).equals(entry.getValue())) {
//不相等时,不需要删除改行记录,更新filter
needDelete = false;
for (String s : originDistinctFields) {
mapFilter.put(s, recordMap.get(s));
}
break;
}
}
if(needDelete) {
//重复数据都需要删除
resp.remove(i);
i -= 1;
}
}
if(originGroupByFields.size() == originDistinctFields.size()) {
return resp;
}
//当groupFields.size < distinctFields.size, 需要手动聚合
List<Map<String, Object>> res = new ArrayList<>();
Map<String, Object> tempMap = new HashMap<>();
firstRecord = resp.get(0);
for(GatherField gatherField : originQuotas) {
tempMap.put(gatherField.getField(),firstRecord.get(gatherField.getField()));
}
res.add(tempMap);
boolean needInsert;
for(int i = 1; i < resp.size(); i ++) {
//如果res集合中groupByField相同,则需要聚合; 否则插入即可
recordMap = resp.get(i);
needInsert = true;
boolean isDuplicate; //是否重复
//对于每一行记录,检查res中是否有相同的
for(int j = 0; j < res.size(); j ++) {
isDuplicate = true;
tempMap = res.get(j);
for(String s : originGroupByFields) {
if(!tempMap.get(s).equals(recordMap.get(s))) {
//不相等, 继续比较res的下一个元素
isDuplicate = false;
break;
}
}
if(isDuplicate) {
//如果当前元素重复, 那么就覆盖,不需要插入
needInsert = false;
//TODO:recordMap 覆盖tempMap
for(GatherField gatherField : quotasFields) {
switch (gatherField.getType()) {
case SUM:
case COUNT:
tempMap.put(gatherField.getField(), (double)tempMap.get(gatherField.getField()) + (double)recordMap.get(gatherField.getField()));
continue;
case AVG:
tempMap.put(gatherField.getField(), ((double)tempMap.get(gatherField.getField()) + (double)recordMap.get(gatherField.getField()))/2.0);
continue;
case MAX:
tempMap.put(gatherField.getField(), Math.max((double)tempMap.get(gatherField.getField()), (double)recordMap.get(gatherField.getField())));
continue;
case MIN:
tempMap.put(gatherField.getField(), Math.min((double)tempMap.get(gatherField.getField()), (double)recordMap.get(gatherField.getField())));
}
}
break;
}
}
//遍历完成之后,如果需要插入
if(needInsert) {
//插入res,是完成后才插入,不需要变换j
Map<String, Object> node = new HashMap<>();
for(GatherField gatherField : originQuotas) {
node.put(gatherField.getField(),recordMap.get(gatherField.getField()));
}
res.add(node);
}
}
return res;
}
使用Stream优化
private List<Map<String, Object>> generateData() {
List<Map<String, Object>> resp = new ArrayList<>();
Map<String, Object> record = new HashMap<>();
record.put("商城名称", "招商大魔方");
record.put("商城账户", "23513153451145");
record.put("成交时间", "2023/02/23");
record.put("回执单数", 10);
record.put("交易总数", 9);
record.put("更新日期", "2023-02-23 14:59:20");
resp.add(record);
Map<String, Object> record1 = new HashMap<>();
record1.put("商城名称", "宏帆广场");
record1.put("商城账户", "1235353451145");
record1.put("成交时间", "2023/02/23");
record1.put("回执单数", 7);
record1.put("交易总数", 9);
record1.put("更新日期", "2023-02-23 14:59:20");
resp.add(record1);
Map<String, Object> record2 = new HashMap<>();
record2.put("商城名称", "招商大魔方");
record2.put("商城账户", "23513153451145");
record2.put("成交时间", "2023/02/24");
record2.put("回执单数", 6);
record2.put("交易总数", 6);
record2.put("更新日期", "2023-02-24 14:59:20");
resp.add(record2);
Map<String, Object> record3 = new HashMap<>();
record3.put("商城名称", "农业广场");
record3.put("商城账户", "23513153458123");
record3.put("成交时间", "2023/02/25");
record3.put("回执单数", 10);
record3.put("交易总数", 8);
record3.put("更新日期", "2023-02-25 13:59:20");
resp.add(record3);
Map<String, Object> record4 = new HashMap<>();
record4.put("商城名称", "宏帆广场");
record4.put("商城账户", "1235353451145");
record4.put("成交时间", "2023/02/23");
record4.put("回执单数", 5);
record4.put("交易总数", 3);
record4.put("更新日期", "2023-02-23 14:59:20");
resp.add(record4);
Map<String, Object> record5 = new HashMap<>();
record5.put("商城名称", "招商大魔方");
record5.put("商城账户", "23513153451145");
record5.put("成交时间", "2023/02/24");
record5.put("回执单数", 6);
record5.put("交易总数", 6);
record5.put("更新日期", "2023-02-24 14:59:20");
resp.add(record5);
Map<String, Object> record6 = new HashMap<>();
record6.put("商城名称", "农业广场");
record6.put("商城账户", "23513153458123");
record6.put("成交时间", "2023/02/25");
record6.put("回执单数", 10);
record6.put("交易总数", 8);
record6.put("更新日期", "2023-02-25 13:59:20");
resp.add(record6);
Map<String, Object> record7 = new HashMap<>();
record7.put("商城名称", "万达广场");
record7.put("商城账户", "2351387834758123");
record7.put("成交时间", "2023/02/23");
record7.put("回执单数", 5);
record7.put("交易总数", 3);
record7.put("更新日期", "2023-02-23 14:59:20");
resp.add(record7);
Map<String, Object> record8 = new HashMap<>();
record8.put("商城名称", "艾尔摩尔");
record8.put("商城账户", "123243543634112");
record8.put("成交时间", "2023/02/24");
record8.put("回执单数", 6);
record8.put("交易总数", 6);
record8.put("更新日期", "2023-02-24 14:59:20");
resp.add(record8);
Map<String, Object> record9 = new HashMap<>();
record9.put("商城名称", "华夏摩登");
record9.put("商城账户", "123445568123679");
record9.put("成交时间", "2023/02/27");
record9.put("回执单数", 14);
record9.put("交易总数", 14);
record9.put("更新日期", "2023-02-27 13:59:20");
resp.add(record9);
return resp;
}
List<String> primaryFields = Arrays.asList("商城名称", "商城账户", "成交时间");
String groupField = "成交时间"; //按照商城名称分组
List<String> sumFields = Arrays.asList("回执单数", "交易总数");
//mock数据
List<Map<String, Object>> mockData = generateData();
//去重, 将primaryKey拼接在一起为key, Map当作value, 这样只需要填充map即可,最后取所有的value进行后操作
HashMap<String, Map<String, Object>> deduplicatedMap = new HashMap<>();
System.out.println("去重前的总数:" + mockData.size());
for(Map<String, Object> betaData: mockData) {
StringBuilder primaryValue = new StringBuilder();
for(Map.Entry<String, Object> entry : betaData.entrySet()) {
//拼接primary的值
if(primaryFields.contains(entry.getKey())) {
primaryValue.append(entry.getValue());
}
}
//第一个可以put入,后面的就不能放入,可以使用map的putIfAbsent方法
deduplicatedMap.putIfAbsent(primaryValue.toString(),betaData);
}
System.out.println("去重后的总数: " + deduplicatedMap.size());
System.out.println(deduplicatedMap.values());
//需要对结果进行分组之后进行求和等相关操作,使用Stream
//groupBy后,数据就是groupFiled : List<原对象>, 按照groupField进行分组划分为多个list
//对于多个list, 最后需要合并为一个stream,所以使用flatMap【合并stream,所以flatMap中应该为stream】
//对于每组list,需要求给定的sumFields的和, 使用reduce【返回Stream一个值,max/min/sum/avg为特例】,返回的是Optional,需要get
List<Map<String, Object>> res = deduplicatedMap.values().stream().collect(Collectors.groupingBy(map -> map.get(groupField)))
.values().stream().flatMap(list -> Stream.of(list.stream().reduce((m1, m2) -> {
//T, T -> T
//对分组内的sunFields求和, m1和m2就是抽象的list中的两个map【一次迭代】
for(String sumField : sumFields) {
double value1 = Double.parseDouble(m1.get(sumField).toString());
double value2 = Double.parseDouble(m2.get(sumField).toString());
m1.put(sumField, value1 + value2);
}
return m1;
}).get())).collect(Collectors.toList());
System.out.println(res);
[
{更新日期=2023-02-23 14:59:20, 成交时间=2023/02/23, 商城账户=1235353451145, 商城名称=宏帆广场, 回执单数=7, 交易总数=9},
{更新日期=2023-02-25 13:59:20, 成交时间=2023/02/25, 商城账户=23513153458123, 商城名称=农业广场, 回执单数=10, 交易总数=8},
{更新日期=2023-02-24 14:59:20, 成交时间=2023/02/24, 商城账户=23513153451145, 商城名称=招商大魔方, 回执单数=6, 交易总数=6},
{更新日期=2023-02-23 14:59:20, 成交时间=2023/02/23, 商城账户=23513153451145, 商城名称=招商大魔方, 回执单数=10, 交易总数=9},
{更新日期=2023-02-23 14:59:20, 成交时间=2023/02/23, 商城账户=2351387834758123, 商城名称=万达广场, 回执单数=5, 交易总数=3},
{更新日期=2023-02-27 13:59:20, 成交时间=2023/02/27, 商城账户=123445568123679, 商城名称=华夏摩登, 回执单数=14, 交易总数=14},
{更新日期=2023-02-24 14:59:20, 成交时间=2023/02/24, 商城账户=123243543634112, 商城名称=艾尔摩尔, 回执单数=6, 交易总数=6}
]
# 按照商城账户分组求和
[
{更新日期=2023-02-25 13:59:20, 成交时间=2023/02/25, 商城账户=23513153458123, 商城名称=农业广场, 回执单数=10, 交易总数=8},
{更新日期=2023-02-24 14:59:20, 成交时间=2023/02/24, 商城账户=23513153451145, 商城名称=招商大魔方, 回执单数=16.0, 交易总数=15.0},
{更新日期=2023-02-24 14:59:20, 成交时间=2023/02/24, 商城账户=123243543634112, 商城名称=艾尔摩尔, 回执单数=6, 交易总数=6},
{更新日期=2023-02-23 14:59:20, 成交时间=2023/02/23, 商城账户=1235353451145, 商城名称=宏帆广场, 回执单数=7, 交易总数=9},
{更新日期=2023-02-27 13:59:20, 成交时间=2023/02/27, 商城账户=123445568123679, 商城名称=华夏摩登, 回执单数=14, 交易总数=14},
{更新日期=2023-02-23 14:59:20, 成交时间=2023/02/23, 商城账户=2351387834758123, 商城名称=万达广场, 回执单数=5, 交易总数=3}
]
# 按照成交时间分组求和
[
{更新日期=2023-02-25 13:59:20, 成交时间=2023/02/25, 商城账户=23513153458123, 商城名称=农业广场, 回执单数=10, 交易总数=8},
{更新日期=2023-02-27 13:59:20, 成交时间=2023/02/27, 商城账户=123445568123679, 商城名称=华夏摩登, 回执单数=14, 交易总数=14},
{更新日期=2023-02-24 14:59:20, 成交时间=2023/02/24, 商城账户=23513153451145, 商城名称=招商大魔方, 回执单数=12.0, 交易总数=12.0},
{更新日期=2023-02-23 14:59:20, 成交时间=2023/02/23, 商城账户=1235353451145, 商城名称=宏帆广场, 回执单数=22.0, 交易总数=21.0}
]