• Java8中那些方便又实用的Map函数


    原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。

    简介#

    java8之后,常用的Map接口中添加了一些非常实用的函数,可以大大简化一些特定场景的代码编写,提升代码可读性,一起来看看吧。

    computeIfAbsent函数#

    比如,很多时候我们需要对数据进行分组,变成Map>的形式,在java8之前,一般如下实现:

    List payments = getPayments();
    Map> paymentByTypeMap = new HashMap<>();
    for(Payment payment : payments){
        if(!paymentByTypeMap.containsKey(payment.getPayTypeId())){
            paymentByTypeMap.put(payment.getPayTypeId(), new ArrayList<>());
        }
        paymentByTypeMap.get(payment.getPayTypeId())
                .add(payment);
    }
    

    可以发现仅仅做一个分组操作,代码却需要考虑得比较细致,在Map中无相应值时需要先塞一个空List进去。

    但如果使用java8提供的computeIfAbsent方法,代码则会简化很多,如下:

    List payments = getPayments();
    Map> paymentByTypeMap = new HashMap<>();
    for(Payment payment : payments){
        paymentByTypeMap.computeIfAbsent(payment.getPayTypeId(), k -> new ArrayList<>())
                .add(payment);
    }
    

    computeIfAbsent方法的逻辑是,如果map中没有(Absent)相应的key,则执行lambda表达式生成一个默认值并放入map中并返回,否则返回map中已有的值。

    带默认值Map
    由于这种需要默认值的Map太常用了,我一般会封装一个工具类出来使用,如下:

    public class DefaultHashMap extends HashMap {
        Function function;
    
        public DefaultHashMap(Supplier supplier) {
            this.function = k -> supplier.get();
        }
    
        @Override
        @SuppressWarnings("unchecked")
        public V get(Object key) {
            return super.computeIfAbsent((K) key, this.function);
        }
    }
    

    然后再这么使用,如下:

    List payments = getPayments();
    Map> paymentByTypeMap = new DefaultHashMap<>(ArrayList::new);
    for(Payment payment : payments){
        paymentByTypeMap.get(payment.getPayTypeId())
                .add(payment);
    }
    

    呵呵,这玩得有点像python的defaultdict(list)了😁

    临时Cache
    有时,在一个for循环中,需要一个临时的Cache在循环中复用查询结果,也可以使用computeIfAbcent,如下:

    List payments = getPayments();
    Map payTypeCacheMap = new HashMap<>();
    for(Payment payment : payments){
        PayType payType = payTypeCacheMap.computeIfAbsent(payment.getPayTypeId(), 
                k -> payTypeMapper.queryByPayType(k));
        payment.setPayTypeName(payType.getPayTypeName());
    }
    

    因为payments中不同payment的pay_type_id极有可能相同,使用此方法可以避免大量重复查询,但如果不用computeIfAbcent函数,代码就有点繁琐晦涩了。

    computeIfPresent函数#

    computeIfPresent函数与computeIfAbcent的逻辑是相反的,如果map中存在(Present)相应的key,则对其value执行lambda表达式生成一个新值并放入map中并返回,否则返回null。

    这个函数一般用在两个集合做等值关联的时候,可少写一次判断逻辑,如下:

    @Data
    public static class OrderPayment {
        private Order order;
        private List payments;
    
        public OrderPayment(Order order) {
            this.order = order;
            this.payments = new ArrayList<>();
        }
    
        public OrderPayment addPayment(Payment payment){
            this.payments.add(payment);
            return this;
        }
    }
    
    public static void getOrderWithPayment(){
        List orders = getOrders();
        Map orderPaymentMap = new HashMap<>();
        for(Order order : orders){
            orderPaymentMap.put(order.getOrderId(), new OrderPayment(order));
        }
        List payments = getPayments();
        //将payment关联到相关的order上
        for(Payment payment : payments){
            orderPaymentMap.computeIfPresent(payment.getOrderId(),
                    (k, orderPayment) -> orderPayment.addPayment(payment));
        }
    }
    

    compute函数#

    compute函数,其实和computeIfPresent、computeIfAbcent函数是类似的,不过它不关心map中到底有没有值,都执行lambda表达式计算新值并放入map中并返回。

    这个函数适合做分组迭代计算,像分组汇总金额的情况,就适合使用compute函数,如下:

    List payments = getPayments();
    Map amountByTypeMap = new HashMap<>();
    for(Payment payment : payments){
        amountByTypeMap.compute(payment.getPayTypeId(), 
                (key, oldVal) -> oldVal == null ? payment.getAmount() : oldVal.add(payment.getAmount())
        );
    }
    

    当oldValue是null,表示map中第一次计算相应key的值,直接给amount就好,而后面再次累积计算时,直接通过add函数汇总就好。

    merge函数#

    可以发现,上面在使用compute汇总金额时,lambda表达式中需要判断是否是第一次计算key值,稍微麻烦了点,而使用merge函数的话,可以进一步简化代码,如下:

    List payments = getPayments();
    Map amountByTypeMap = new HashMap<>();
    for(Payment payment : payments){
        amountByTypeMap.merge(payment.getPayTypeId(), payment.getAmount(), BigDecimal::add);
    }
    

    这个函数太简洁了😄,merge的第一个参数是key,第二个参数是value,第三个参数是值合并函数。
    当是第一次计算相应key的值时,直接放入value到map中,后面再次计算时,使用值合并函数BigDecimal::add计算出新的汇总值,并放入map中即可。

    putIfAbsent函数#

    putIfAbsent从命名上也能知道作用了,当map中没有相应key时才put值到map中,主要用于如下场景:
    如将list转换为map时,若list中有重复值时,put与putIfAbsent的区别如下:

    • put保留最晚插入的数据。
    • putIfAbsent保留最早插入的数据。

    forEach函数#

    说实话,java中要遍历map,写法上是比较啰嗦的,不管是entrySet方式还是keySet方式,如下:

    for(Map.Entry entry: amountByTypeMap.entrySet()){
        Integer payTypeId = entry.getKey();
        BigDecimal amount = entry.getValue();
        System.out.printf("payTypeId: %s, amount: %s \n", payTypeId, amount);
    }
    

    再看看在python或go中的写法,如下:

    for payTypeId, amount in amountByTypeMap.items():
        print("payTypeId: %s, amount: %s \n" % (payTypeId, amount))
    

    可以发现,在python中的map遍历写法要少写好几行代码呢,不过,虽然java在语法层面上并未支持这种写法,但使用map的forEach函数,也可以简化出类似的效果来,如下:

    amountByTypeMap.forEach((payTypeId, amount) -> {
        System.out.printf("payTypeId: %s, amount: %s \n", payTypeId, amount);
    });
    

    总结#

    一直以来,java因代码编写太繁琐而被开发者们所广泛诟病,但从java8开始,从Map、Stream、var、multiline-string再到record,java在代码编写层面做了大量的简化,java似乎开窍了🤔

  • 相关阅读:
    ADCIRC模式与Python融合技术应用
    【Unity2D】关卡编辑好帮手——TileMap
    数据结构与算法一:开篇
    Si4010 一款带有MCU SoC RF发射机芯片 无线遥控器
    用Wokwi仿真ESP-IDF项目
    校园综合服务平台V3.9.2 源码修复大部分已知BUG
    Spring Data Elasticsearch集成SpringBoot 2.3.12.RELEASE
    vue使用百度地图实现地点查询
    [重庆思庄每日技术分享]-SQLLOADER express加载数据报 KUP-04040
    这样讲Redis 主从复制的工作原理,或许你真的能听懂~
  • 原文地址:https://www.cnblogs.com/codelogs/p/16883736.html