随便看看
发布于 2026-04-30 / 9 阅读
0

在写后台大屏数据接口碰见了一个问题,面对Map<String, Map<String, Object>>这种类型,我希望获取内层 Map下指定key的value累加,也就是内存Map中大部分都有几个相同的Key,比如合同金额,收款金额,我想通过便捷的方式获取统计

换一种说法贴合业务理解,这个结构是外层Map key表示流程ID也就是唯一标识,value也就是内层Map代表得是这个流程下所有得表单值,通过key,value存储,这样说应该好理解了吧?

如何累加通过key的value?

  1. 通过foreach循环 我第一个想到的方法是

    // 模拟数据:Map<流程实例ID, Map<变量名, 变量值>>
            Map<String, Map<String, Object>> processValues = new HashMap<>();
    ​
            // 流程实例1
            Map<String, Object> variables1 = new HashMap<>();
            variables1.put("amount", 1000);
            variables1.put("quantity", 5);
            variables1.put("approver", "张三");
            variables1.put("status", "已完成");
            processValues.put("proc_001", variables1);
    ​
            // 流程实例2
            Map<String, Object> variables2 = new HashMap<>();
            variables2.put("amount", 2500);
            variables2.put("quantity", 8);
            variables2.put("approver", "李四");
            variables2.put("status", "进行中");
            processValues.put("proc_002", variables2);
    ​
            // 流程实例3
            Map<String, Object> variables3 = new HashMap<>();
            variables3.put("amount", 1500);
            variables3.put("quantity", 3);
            variables3.put("approver", "王五");
            variables3.put("status", "已完成");
            processValues.put("proc_003", variables3);

    第一种实现 也是最简单的
    Map<String, Object> sumMap = new HashMap<>();
    // 通过遍历累加
    for (Map<String, Object> value : processValues.values()) {
          int amount = (Integer) sumMap.get("amount");
          int receivedAmount = (Integer) sumMap.get("quantity");
          sumMap.put("amount", amount + (Integer) value.get("amount"));
          sumMap.put("quantity", receivedAmount + (Integer) value.get("quantity"));
    }
  2. 通过stream流的reduce实现

    项目中很少用到,但是这次有时间我就研究一下,有时候改变编程习惯,面对困难也有选择的余地.

    1. reduce的定义

      reduce 是 Stream API 中的一个归约操作,用于将流中的多个元素反复组合,最终得到一个单一的结果

      这里说的最终单一 并不是说结果只是一个值,也可以是对象,对象里面也可以有多个值,别理解错了。比如我们上面这个Map要的效果并不是把 map 里面的多个key合成一个,而是不同key分别累计,最终达到不同key的value值单一

    2. stream流中reduce的三种方法

      1. Optional<T> reduce(BinaryOperator<T> accumulator);
      ​
      先讲第一个方法, 这个方法只需要一个参数,并且返回值是Optional类型
      a. Optional类型是什么?
          一个容器类,也可以理解为一个包装类,通过方法把对象|值 存入里面,他提供了一系列链式调用来处理,比如常用的 判断一个对象是否为空以及里面某个值是否为?来进行不同的逻辑
          // 传统方式 - 需要手动判空
      String name = null;
      if (user != null) {
          name = user.getName();
      }
      ​
      // Optional 方式 - 容器封装了判空逻辑
      String name = Optional.ofNullable(user)
          .map(User::getName)
          .orElse("Unknown");
      
      > 通过这段源码可以理解我们写一些util类,可以通过返回optional<T> 来简化调用方的判断处理逻辑🤔
      ​下图说明了of 和 ofNullable的区别,相对于调用Nullable健壮性更好,在为null的情况下,他会创建一个空的Optional对象
      

b. BinaryOperator的定义
    一个函数式接口,从源码来看泛型<T,T,T> 传入2个相同类型的参数返回一个也是同类型的参数.
    继承自BiFunction,BiFunction的泛型是 <T, U, R> 接收一个T类型 操作一个U函数 返回R类型
    从BinaryOperator的官方备注可以看到,这个是对BiFunction的专业化,只针对同类型的操作. 简单理解的话 这些接口是官网 为了提供在不同情况下的 规范标准,理解是个规范就可以了,就和List,Set集合都继承自Collection是一样的
    
    示例1:
    Optional<Integer> amount = processValues.values().stream().map(o -> (Integer)o.get("amount"))
        .reduce((m, i) -> {
            m += i;
            return m;
        });
​
    结果返回: Optional[5000]
​
    示例2: 通过lambda表达式优化
​
Optional<Integer> amount = processValues.values().stream().map(o -> (Integer)o.get("amount")).reduce(Integer::sum);

    2. T reduce(T identity, BinaryOperator<T> accumulator);
    
        相比于第一个单入参方法,这个方法需要额外传入一个identity,可以理解为初始值
    ⬇️下面是源码的解释
            T result = identity; 
          for (T element : this stream)     {
            result = accumulator.apply(result, element) 
            return result;
            }
        也就是说这个方法支持对累计初始化值可以自定义,而第一个只能通过元素1+2+N
        注意这里泛型都是T,也就是他限制了调用传入的 初始值 identity| 累计函数 accumulator 都必须类型一致,最后返回值也是T 
    
    
1. 2个参数的reduce备注中有一段需要注意的点

执行并不是按照顺序的,下面也提到了 ```结合函数``` 也可以理解为数学的结合律

结合律是什么?

相同运算符的表达式中,只要运算符的位置不变,运算顺序对结果没有影响

1. 加法中✔️

(1 + 2) + 3 = 1 + (2 + 3)

2. 减法中✖️

(10 - 5) - 2 ≠ 10 - (5 - 2) 这种情况

因为定义Stream流分为 stream() 和 parallelStream()[支持并行] ,不过我还没用过并行

3. <U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);

    这个方法 又在第二种方法的基础上扩展了一个参数
  相比于2入参方法,这里有个细节就是accumulator变成了BiFunction类型,通过源码可以看到 BiFunction<T, U, R>正是之前单入参 BinaryOperator的父接口,且他的入参类型T和返回结果R是允许不同的,2个入参用的 BinaryOperator<T> extends BiFunction<T,T,T> 所以说2个参数与返回值都必须同一类型
        
        第三个参数 combiner 表示组合函数,因为parallelStream并行流的存在,所以支持一个集合拆分多个执行,但是最后怎么合并呢? 这时候就需要通过第三个入参设置合并数据逻辑 他支持传入2个参数,也就是一次只能处理2个结果的操作,但是最终会把这些结果单一化输出
        这个时候有个问题,既然是并行为什么不能一次处理多条数据组合呢?
        1. 两两组合效率更高
        2. BiFunction接口下 R apply(T t, U u) 也只定义了2个 而BinaryOperator有对其限制 BiFunction<T,T,T> 所以为什么 BinaryOperator 定义函数是2个一样的值
    示例1:
    List<String> words = Arrays.asList("Hello", " ", "World", "!", " ", "Java");
        String result = words.stream()
            .reduce(
                "",                                    // identity: 初始值
                (partial, word) -> partial + word,    // accumulator: 累加器
                (r1, r2) -> r1 + r2                   // combiner: 合并器
            );
    解释这个示例,words集合开始遍历 
    第一个参数设置为 "" 表示初始值不做修改 走默认拼接
    第二个参数设置 从逻辑上开就是每次用初始值+words迭代的元素 即:
        第一次迭代 => "" + "Hello"
        第二次迭代 => "Hello" + " "
        第三次迭代 => "Hello " + "!"
     第三个参数 要在parallelStream下才会被调用,用stream()自定义也不会生效
    模拟运行逻辑
        原始数据: ["Hello", " ", "World", "!", " ", "Java"]
         ↓ Fork/Join 框架分割
        
        假设分割成 3 个任务(实际取决于CPU核心数):
        任务1: ["Hello", " "]      → 线程A处理
        任务2: ["World", "!"]      → 线程B处理  
        任务3: [" ", "Java"]       → 线程C处理
        然后到这里就会把这3个任务传入到(r1, r2)
        再根据-> r1 + r2   这里的逻辑进行组合
​
​
🫱 实际操作下来发现面对复杂对象还是通过Collect链式处理更好,如果是单一集合用reduce效果更好更快
        

总结

  1. reduce可以对 任意类型的 集合/流 进行聚合操作

  2. stream流默认提供了3个方法

    1. 单参数 只需要我们提供结合规则

    2. 双参数 支持自定义一个初始累计值,但是类型必须一致

    3. 三参数 用于定义并行下 最终计算值的组合输出

  3. 支持并行操作,需使用parallelStream()引用 如果不是则第三个组合参数不会生效

  4. 并行执行要考虑 结合律 问题

ps: 额外知识

  1. Optional<T> 接口 可以包装指定对象,让对象在程序中显示操作,而不是靠被动报错NPE来处理

  2. BiFunction|BinaryOperator 通过传入2个参数返回一个值的 规则类,明白了泛型不止于一个<T>,还能通过 ,逗号 去定一个多个类型