天天看點

Java8中那些友善又實用的Map函數

原創:扣釘日記(微信公衆号ID:codelogs),歡迎分享,轉載請保留出處。

簡介

java8之後,常用的Map接口中添加了一些非常實用的函數,可以大大簡化一些特定場景的代碼編寫,提升代碼可讀性,一起來看看吧。

computeIfAbsent函數

比如,很多時候我們需要對資料進行分組,變成Map<Integer, List<?>>的形式,在java8之前,一般如下實作:

List<Payment> payments = getPayments();
Map<Integer, List<Payment>> 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<Payment> payments = getPayments();
Map<Integer, List<Payment>> 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<K, V> extends HashMap<K, V> {
    Function<K, V> function;

    public DefaultHashMap(Supplier<V> supplier) {
        this.function = k -> supplier.get();
    }

    @Override
    @SuppressWarnings("unchecked")
    public V get(Object key) {
        return super.computeIfAbsent((K) key, this.function);
    }
}
           

然後再這麼使用,如下:

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

呵呵,這玩得有點像python的defaultdict(list)了

臨時Cache

有時,在一個for循環中,需要一個臨時的Cache在循環中複用查詢結果,也可以使用computeIfAbcent,如下:

List<Payment> payments = getPayments();
Map<Integer, PayType> 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<Payment> 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<Order> orders = getOrders();
    Map<Long, OrderPayment> orderPaymentMap = new HashMap<>();
    for(Order order : orders){
        orderPaymentMap.put(order.getOrderId(), new OrderPayment(order));
    }
    List<Payment> 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<Payment> payments = getPayments();
Map<Integer, BigDecimal> 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<Payment> payments = getPayments();
Map<Integer, BigDecimal> 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<String, BigDecimal> 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似乎開竅了