【cfeng work】WorkProj中Stream流的应用详细介绍

WorkProj

内容管理

  • Stream
    • 中间操作 (流不会关闭)
      • filter(x -> x boolean表达式) 筛选返回值为true的
      • distinct() 去重
      • limit(X) 返回前x个元素
      • skip(x) 跳过流中元素
      • map(x操作) 对每一个元素映射操作
      • mapToInt() 将对象流转为数值流 Integer --> int
      • flatMap(x操作) 将一个流中每一个值转为流 【针对对象为集合/array】合并集合
      • sorted(Comparator.comparing(字段)) 将stream中元素排序
    • 终端操作 (关闭流), 返回值
      • allMatch(x -> x boolean表达式) 当stream中所有元素满足条件返回true
      • anyMatch(x -> x boolean表达式) 当stream中一个元素满足就返回true
      • noneMatch(x -> x boolean表达式) 全部不满足返回true
      • count() 统计流中元素个数
      • findFirst() 返回流中第一个元素
      • findAny() 返回流中随机一个元素
      • max/min () 返回流中的最大最小值
      • sum() 求和
      • foreach() 遍历
      • reduce 将流中的元素组合 ⭐ 可替代max、min、sum等
      • collect 返回集合 collect策略有很多 ---> ⭐ 可执行sum、average...等
        • 类型归纳 --- 转为集合容器 toXXX
        • joining 将元素用某规则连接起来
        • collectingAndThen 先对结果进行归纳,之后再进行Function函数处理结果
        • groupingBy 按照条件对结果分组
        • counting 对收集的结果求和
        • mapping(mapper, downSream) 在Collect中进行隐射
        • maxBy(comparator)/minBy
        • summingInt/Double/Long(元素 -- 可用Lambda)
        • summarizingDouble/Int/Long⭐ 统计量: 包括总数 和 max/min 平均值
        • reducing ⭐ (T, T) -> T 元素两两比较淘汰一个(一轮轮reduce)
    • workProj中应用
      • 不使用Stream(假设数据按照key有序)
      • 使用Stream优化


本文introduce 对于Stream的使用


在关系型数据库中处理数据还是很easy的,使用group by , order by等关键字可以方便查出各种数据

但是很多场景下数据不是在数据库中处理, 而是直接拎到内存中处理, 这个时候Stream就可以发挥巨大作用【相较于传统的for遍历】

比如java的dynamicSqlBuilder, 动态SQL查询出动态数据库表中的数据【所有都是动态的】, 这里的返回值就是List>

要对List>进行处理, 通过Stream的方式就是最快的

流stream是支持数据处理操作的源生成的元素序列, 源可以是数组、文件、集合、函数; Stream不是集合,不是数据结构,主要目的就是计算

Stream

Stream创建方式一共有5种, 集合、数组、of值、文件、函数iterator无限流

  • 最常见的就是通过集合Collection生成
Stream stream = distinctMap.values().stream

除此之外,也可以通过数组Arrays.sttream(数组)、 Stream.of(值1, 值2…)、Files.lines(文件)、 Stream.iterator(xxx)无限流 | Stream.generator(xxx)无限流

流Stream的操作分为两种

  • 中间操作: 一个流后面可以跟随0或者多个中间操作,打开流做出某种程度的映射, 操作是惰性化的,没有真正开始流的遍历, 常见的map、filter都是
  • 终端操作: 一个流只能有一个终端操作,进行流的遍历,操作执行后,流就关闭了,不能再操作,因此一个流只能遍历一次,如collect、count

中间操作 (流不会关闭)

中间操作可以为 0 ~ 多个, 一定程度的映射和操作

比如List list = Arrays.asList(1,1,2,3,4,5,6);

filter(x -> x boolean表达式) 筛选返回值为true的

list.stream().filter(item -> item > 3)
 
//这里的item代表的就是流中的每一个元素,可以使用Lambda表达式
//4,5,6    

distinct() 去重

list.stream().distinct()
    
//结果1,2,3,4,5,6    

limit(X) 返回前x个元素

这个和数据库limit类似

list.stream().limit(3)
    
//1,1,2    

skip(x) 跳过流中元素

和limit相反,跳过前X个元素

list.stream().skip(3)
    
//3,4,5,6    

map(x操作) 对每一个元素映射操作

操作位置可以使用Lambda或者函数引用

list.stream().map(x -> x + 1)
    
//2,2,3,4,5,6,7

mapToInt() 将对象流转为数值流 Integer --> int

flatMap(x操作) 将一个流中每一个值转为流 【针对对象为集合/array】合并集合

这个和Map的不同主要就是针对List>的情况, 一层流只能得到List, 要想直接得到最内层对象,就只能flatMap, 会将所有的List全转化为流

List> strList = [[“java,python”], [“cfeng”,“cshen”]]

strList.stream()
    .flatMap(item -> item.Stream())

sorted(Comparator.comparing(字段)) 将stream中元素排序

这个sorted中可以给出字段,就是按照默认的顺序排序,加上.reversed()就可以倒序,当然也可以使用Lambda表达式进行定制的排序

list.stream.sorted(Comparator.comparing(Book:: getPublishTime).reversed())

终端操作 (关闭流), 返回值

终端操作只能一个, 会对流进行遍历(和requestBody一样,只能一次性)

allMatch(x -> x boolean表达式) 当stream中所有元素满足条件返回true

if(list.stream.allMatch(item -> item > 3))
    
//这里false,因为1,1,2,3几个元素不满足    

anyMatch(x -> x boolean表达式) 当stream中一个元素满足就返回true

if(list.stream.anyMatch(item -> item > 5))
    
//true,因为6这个元素满足    

noneMatch(x -> x boolean表达式) 全部不满足返回true

if(list.stream.noneMatch(item -> item > 5))
    
//false,因为6这个元素满足    

count() 统计流中元素个数

list.stream().count()
//个数7  

findFirst() 返回流中第一个元素

list.stream().findFirst()
//1

findAny() 返回流中随机一个元素

list.stream().findAny()
//4 , 随机的    

max/min () 返回流中的最大最小值

list.stream().min(Integer::CompareTo)

list.stream().min()

//1

sum() 求和

list.stream().sum() //可以用lambda统计元素, 比如str.length求和
   
//1 + 1 + 2 + 3 + 4 + 5 + 6    

foreach() 遍历

list.stream().foreach(System::println);
//1 /n 1 .....

reduce 将流中的元素组合 ⭐ 可替代max、min、sum等

(T,T) -> T, 不断reduce, 两两比较; 其实max、min、sum都是reduce的特例,一个是两两比大小,一个是两两求和

其中的操作可以用迭代的思想看就是 一次迭代【通用计算方式】, 之后程序内部就会按照公式不断两两操作得到最后的结果

reduce可以实现从Stream中生成一个值,不是随意的,根据指定的模型

  • reduce(BinaryOperator

    参数列表为一个函数式接口

    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
    }
    

    可以看到就是接受T, U,之后返回R, 默认实现为minBy和maxBy就是取stream的最值,最要代码就是(a,b) -> comparator.compare(a, b) >= 0 ? a : b; 返回一个值

    //比如实现求和操作, 这里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 返回集合 collect策略有很多 —> ⭐ 可执行sum、average…等

最常见的collect就是Collectors.toList()策略, 就是将流转为一个List

类型归纳 — 转为集合容器 toXXX

Collectors.toList();
Collectors.toMap();
Collectors.toSet();
Collectors.toCollection();//含参构造,转化为new的容器
Collectors.toConcurrentMap();

分别将流转为List、Map、Set、Collection、 ConcurrentMap

joining 将元素用某规则连接起来

连接符号可以是delimiter连接符; 或者delimiter连接符、prefix开始符,suffix结束符; 除此之外,还可以空参,代表delimiter为空格

Collectors.joining();  // 以空格连接

Collectors.joining("-");  //以-连接
   
Collectors.joining("【",",","】"); //以【x, y, z.....】

collectingAndThen 先对结果进行归纳,之后再进行Function函数处理结果

结果操作就是将collect结果再执行

List<String> list = Arrays.asList("cfeng", "java", "数字大屏");
String str = list.stream().collect(Collectors.collectingAndThen(Collectors.joining(",","[","]"), s -> s + "你好")); System.out.println(str);

groupingBy 按照条件对结果分组

和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

counting 对收集的结果求和

list.stream().count() 效果一样
list.sream().collect(Collectors.counting())

mapping(mapper, downSream) 在Collect中进行隐射

List<String> names = students.stream().collect(Collectors.mapping(Student::getName, Collectors.toList()));

maxBy(comparator)/minBy

大小元素的操作,和list.stream.max终端操作一样

list.stream().collect(Collectors.minBy(Comparator.comparingInt(String.length)))  //字符串中比较按照长度比较

list.stream().min(Comparator.comparingInt(String.length))

summingInt/Double/Long(元素 – 可用Lambda)

累加操作,和sum()终端操作一样

list.stream().collect(Collectors.summingInt(s -> s.length)))  //字符串中比较按照长度比较

list.stream().min(Comparator.comparingInt(String.length))

summarizingDouble/Int/Long⭐ 统计量: 包括总数 和 max/min 平均值

summarizing对应的就是统计量,会将总数、总和、max、min、avg等统计量提取出来放到IntSummaryStatisics(Double、 Long) 等统计对象中

IntSummaryStatistics  statistics = list.stream().collect(Collectors.summarizingInt(s -> s.length)));

需要的数据再从对象中直接取出即可

reducing ⭐ (T, T) -> T 元素两两比较淘汰一个(一轮轮reduce)

这个方法对用的终端操作也就是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))));

workProj中应用

首先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> ---- 一个map对应的是一个元组

那么这里处理这种List>, 比如进行Group、Order相关SQL操作,使用Stream就很好处理了

现在比如我给一个模糊化的需求: 对于一个数据库表【商城名称、商城账户、成交时间、回执单数、交易总数、更新日期】,其中可能有数据【商城名称、商城账户、成交时间】相同,但是更新日期不同,认为这种数据为重复数据,需要取更新日期最新的数据,处理之后再按照商城名称进行分组求回执单数和交易总数的和

可以看到这里涉及三个操作: 去重 -> Group -> Sum, 去重可以认为是算法,Group、sum就是内存中进行SQL操作,Stream发挥重要作用

不使用Stream(假设数据按照key有序)

其实这就是一个算法题, 首先给一个List

这里给一下出现的数据的封装对象

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)
}

简单容易思考的解法: 设置一个map的过滤器,让数据依次通过这个过滤器,去重之后,再进行group和相关SQL操作(全部都不使用Stream)

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;
  }

可以看到过程非常麻烦,后面的聚合Gather也是使用的手动聚合,flag标志过多,可读性和可维护性差

使用Stream优化

首先就是去重过程,可以看到使用一个Map作为filter的方式可读性很差,我们可以将primaryFields字段拼接为String, 以string为Key, 对应的Map为value进行去重,因为数据按照定义的deduplication字段有序,所以只需要取第一个出现的作为value

这里测试的时候,首先需要mock数据

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());

去重之后对结果进行分组求和,group by, sum()

//需要对结果进行分组之后进行求和等相关操作,使用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}
]

可以看到使用Stream因为可以调用现成的api,所以减少了很多工作量, 只是这个逻辑中需要注意几个地方:

  • flatMap相较于map作用是合并Stream, 所以使用reduce最终获得一个结果后,需要封装为Stream对象,使用Stream.of()
  • reduce其实就是T,T -> T(最简单的应用),就是stream中两个对象的迭代模式,max/min/sum/avg都可以用reduce替代, reduce的功能更强大, reduce需要返回一个对象,最后返回的就是两两迭代后最红一个对象
  • 对于这种List>的处理,需要擅用map的相关values()和putIfAbsent()等方法,同时使用Stream整体的可阅读性更好

你可能感兴趣的:(Work,Road,数据库,java,sql)