Java8 中新特性 compute、putIfAbsent、computeIfAbsent、computeIfPresent、merge 函数等用法,方便又实
2687
2023.02.02
2023.02.02
发布于 未知归属地

1817. 查找用户活跃分钟数说起

这道题,就是分组+统计计数

 public int[] findingUsersActiveMinutes(int[][] logs, int k) {
        int[] ans = new int[k];
        Map<Integer, Set<Integer>> map = new HashMap<>();
        for (int[] log : logs) {
            map.computeIfAbsent(log[0], kk -> new HashSet<>()).add(log[1]);
        }
        for (Set<Integer> set : map.values()) {
            ans[set.size() - 1]++;
        }
        return ans;
    }

思维还是不精炼,java8提供的computeIfAbsent等常用API还是运用,不娴熟

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);
}
	List<String> list = getList();
	Map<String, Set<String>> map = new HashMap<>();
	for (String str : list) {
	    if (map.containsKey(str)) {
	        map.get(str).add("a");
	    } else {
	        map.put(str, new HashSet<>(Collections.singletonList("a")));
	    }
	}

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

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

使用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);
}
List<String> list = getList();
Map<String, Set<String>> map = new HashMap<>();
for (String str : list) {
    Set<String> set = map.computeIfAbsent(str, newSet -> new HashSet<>());
    set.add("a");
}
 resultMap = new HashMap<Integer, List<People>>();
//对5岁以上的人进行分组
for (People people : peopleList) {
    //如果value值不存在,返回的是新的value值
    //如果value存在,则返回的是旧值
    List<People> s = resultMap.computeIfAbsent(people.getAge(), k -> new ArrayList<People>());

    s.add(people);
}
System.out.println(resultMap);
System.out.println("--------------------------");

resultMap = new HashMap<Integer, List<People>>();
//对5岁以上的人进行分组
for (People people : peopleList) {
    //如果value值不存在,返回的是新的value值
    //如果value存在,则返回的是旧值
    List<People> s = resultMap.computeIfAbsent(people.getAge(), k ->{
        if(k>5) {
            return new ArrayList<People>();
        }
        return null;
    });

    if(s!=null) {
        s.add(people);
    }
}
System.out.println(resultMap);

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> cache = new HashMap<>();
for(Payment payment : payments){
    PayType payType = cache.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上,如果orderPaymentMap中存在payment的orderId,则关联相加!
    for(Payment payment : payments){
        orderPaymentMap.computeIfPresent(payment.getOrderId(),
                (k, orderPayment) -> orderPayment.addPayment(payment));
    }
}

运用实例

public static void main(String[] args) {
        // 创建一个 HashMap
        HashMap<String, Integer> orderPaymentMap = new HashMap<>();

        // 往HashMap中添加映射关系
        orderPaymentMap.put("Shoes", 200);
        orderPaymentMap.put("Bag", 300);
        orderPaymentMap.put("Pant", 150);
        System.out.println("HashMap: " + orderPaymentMap);

        // 重新计算鞋加上10%的增值税后的价值
        List<String> payments = new ArrayList<String>();
        payments.add("Shoes");
        payments.add("Pant");
        for (String payment : payments) {
            int shoesPrice = orderPaymentMap.computeIfPresent(payment, (key, value) -> value + value * 30 / 100);
            System.out.println("Price of Shoes after VAT: " + shoesPrice);
        }

        // 输出更新后的HashMap
        System.out.println("Updated HashMap: " + orderPaymentMap);

image.png

将payment关联到相关的order上,类似于SQL的left join关联运算!

compute函数

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

  • 新传入的value不为null:建立映射关系,最终value的值为lamda表达式remappingFunction的执行结果

  • 新传入的value为null时:key已存在,移除该映射关系,返回null;否则,key不存在直接返回null

这个函数适合做分组迭代计算,像分组汇总金额的情况,就适合使用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())
    );
}

实例

jdk1.8之前的写法

 @Test
    public void testNoCompute() {
        String str = "hello java, i am vary happy! nice to meet you";

        // jdk1.8之前的写法
        HashMap<Character, Integer> result1 = new HashMap<>(32);
        for (int i = 0; i < str.length(); i++) {
            char curChar = str.charAt(i);
            Integer curVal = result1.get(curChar);
            if (curVal == null) {
                curVal = 1;
            } else {
                curVal += 1;
            }
            result1.put(curChar, curVal);
        }
        System.out.println(JSON.toJSONString(result1));
    }

jdk1.8的写法

@Test
public void testCompute() {
    String str = "hello java, i am vary happy! nice to meet you";

    // jdk1.8的写法
    HashMap<Character, Integer> result2 = new HashMap<>(32);
    for (int i = 0; i < str.length(); i++) {
        char curChar = str.charAt(i);
        result2.compute(curChar, (k, v) -> {
            if (v == null) {
                v = 1;
            } else {
                v += 1;
            }
            return v;
        });
    }
    System.out.println(JSON.toJSONString(result2));
}

image.png

  • 当oldValue是null,表示map中第一次计算相应key的值,直接给amount就好
  • 而后面再次累积计算时,直接通过add函数汇总就好。
    总结:返回值都是remappingFunction执行完后,Map中最终存储的value值

merge函数

default V merge(K key, V value, BiFunction<V, V, V> remappingFunction) {
    V oldValue = get(key);
    V newValue = (oldValue == null) ? value :
               remappingFunction.apply(oldValue, value);
    if (newValue == null) {
        remove(key);
    } else {
        put(key, newValue);
    }
    return newValue;
}

merge() 适用于两种情况。如果给定的Key值不存在,它就变成了put(key, value)。
但如果所述Key已经存在一些值, remappingFunction 可以操作合并:

只需返回新值覆盖旧值: (old, new) -> new
只需返回旧值保留旧值: (old, new) -> old
以某种方式合并两者,例如: (old, new) -> old + new
甚至删除旧值: (old, new) -> null

可以发现,上面在使用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,第三个参数是值合并函数

使用merge()模拟一个帐户操作

import java.math.BigDecimal;
@Data
@AllArgsConstructor
class Operation {
    String accNo;
    BigDecimal amount;
}
    List<Operation> operations = Stream.of(new Operation("123", new BigDecimal("10")),
            new Operation("456", new BigDecimal("1200")), new Operation("123", new BigDecimal("-4")),
            new Operation("123", new BigDecimal("8")), new Operation("456", new BigDecimal("800")),
            new Operation("456", new BigDecimal("-1500")), new Operation("123", new BigDecimal("2")),
            new Operation("123", new BigDecimal("-6.5")), new Operation("456", new BigDecimal("-600")))
        .collect(Collectors.toList());

    @Test
    public void testIfAbsent() {
        HashMap<String, BigDecimal> balances = new HashMap<String, BigDecimal>();
        operations.forEach(op -> {
            String key = op.getAccNo();
            balances.putIfAbsent(key, BigDecimal.ZERO);
            balances.computeIfPresent(key, (accNo, prev) -> prev.add(op.getAmount()));
        });
        System.out.println(JSON.toJSONString(balances));
    }

    @Test
    public void testMerge1() {
        HashMap<String, BigDecimal> balances = new HashMap<String, BigDecimal>();
        operations.forEach(op -> balances.merge(op.getAccNo(), op.getAmount(), (soFar, amount) -> soFar.add(amount)));

        operations.forEach(op -> balances.merge(op.getAccNo(), op.getAmount(), BigDecimal::add));

        //{123=9.5, 456=-100}
        System.out.println(JSON.toJSONString(balances));
    }

image.png

当是第一次计算相应key的值时,直接放入value到map中,
后面再次计算时,使用值合并函数BigDecimal::add计算出新的汇总值,并放入map中即可。

putIfAbsent函数

putIfAbsent从命名上也能知道作用了,当map中没有相应key时,才put值到map中,主要用于如下场景:

如将list转换为map时,若list中有重复值时,put与putIfAbsent的区别如下:

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

总结:
putIfAbsent不论传入的value,是否为空,都会建立映射(并不适合所有子类,例如HashTable),
而computeIfAbsent和computeIfPresent,都是只有在mappingFunction的value不为null的时候,才会建立映射;
putIfAbsent返回的是执行前Map中的value值,key不存在则返回null

forEach函数

用于两个参数之间进行操作的函数式接口是 BiConsumer。
这个函数式接口正好用来操作 Map 的 key 和 value。
JDK8强化了针对 Map 类的迭代方式,新增了一个默认方法 forEach,它接收一个 BiConsumer 函数。
说实话,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);
});

实例

 @Test
public void testForEach() {
    // 创建一个Map
    Map<String, Object> infoMap = new HashMap<>();
    infoMap.put("name", "Zebe");
    infoMap.put("email", "your email ddress");
    infoMap.put("site_domain", "your site domain");
    infoMap.put("site_title", "CDR插件技术网");
    infoMap.put("site_description", "国内领先的CDR插件|教程|资源|技术平台");
    // 传统的Map迭代方式
    for (Map.Entry<String, Object> entry : infoMap.entrySet()) {
        System.out.println(entry.getKey() + ":" + entry.getValue());
    }
    // JDK8的迭代方式
    infoMap.forEach((key, value) -> {
        System.out.println(key + ":" + value);
    });
    //{123=9.5, 456=-100}
    System.out.println(JSON.toJSONString(infoMap));
}

总结

jdk1.8之前的写法

@Test
    public void testMerge() {
        String[] wos = new String[] {"Foo", "Bar", "Foo", "Buzz", "Foo", "Buzz", "Fizz", "Fizz"};

        List<String> words = Arrays.asList(wos);

        //计算唯一的单词出现次数
        HashMap<String, Integer> map = new HashMap<String, Integer>();
        words.forEach(word -> {
            Integer prev = map.get(word);
            if (prev == null) {
                map.put(word, 1);
            } else {
                map.put(word, prev + 1);
            }
        });
        System.out.println(JSON.toJSONString(map));
    }

jdk1.8之后的各种函数的实现使用

使用putIfAbsent优化

 @Test
    public void testIfAbsent() {
        String[] wos = new String[] {"Foo", "Bar", "Foo", "Buzz", "Foo", "Buzz", "Fizz", "Fizz"};

        List<String> words = Arrays.asList(wos);

        //计算唯一的单词出现次数
        HashMap<String, Integer> map = new HashMap<String, Integer>();
        //进行重构以避免条件逻辑
        words.forEach(word -> {
            map.putIfAbsent(word, 0);
            map.put(word, map.get(word) + 1);
        });
        System.out.println(JSON.toJSONString(map));
    }
  • putIfAbsent()是必要的,否则代码会在第一次出现未知的单词时中断。
  • 另外map.get(word) 在map.put() 里面,有点尴尬。

使用computeIfPresent优化

 @Test
    public void testIfPresent() {
        String[] wos = new String[] {"Foo", "Bar", "Foo", "Buzz", "Foo", "Buzz", "Fizz", "Fizz"};

        List<String> words = Arrays.asList(wos);

        //计算唯一的单词出现次数
        HashMap<String, Integer> map = new HashMap<String, Integer>();
        words.forEach(word -> {
            map.putIfAbsent(word, 0);
            map.computeIfPresent(word, (w, prev) -> prev + 1);
        });
        System.out.println(JSON.toJSONString(map));
    }

computeIfPresent() 仅当question(word)中的键存在时,才调用给定的转换。
否则它什么都不做。

使用compute优化

 @Test
    public void testCompute() {
        String[] wos = new String[] {"Foo", "Bar", "Foo", "Buzz", "Foo", "Buzz", "Fizz", "Fizz"};

        List<String> words = Arrays.asList(wos);

        //计算唯一的单词出现次数
        HashMap<String, Integer> map = new HashMap<String, Integer>();
        words.forEach(word ->
            map.compute(word, (w, prev) -> prev != null ? prev + 1 : 1)
        );
        System.out.println(JSON.toJSONString(map));
    }

compute() 就像是computeIfPresent(),无论给定key是否存在,都会调用它。
如果键的值不存在,则prev参数为null。

使用merge优化

 @Test
    public void testMerge() {
        String[] wos = new String[] {"Foo", "Bar", "Foo", "Buzz", "Foo", "Buzz", "Fizz", "Fizz"};

        List<String> words = Arrays.asList(wos);

        //计算唯一的单词出现次数
        HashMap<String, Integer> map = new HashMap<String, Integer>();
        //1 的下word ,如果没有key就将1添加到现有值。
        words.forEach(word ->
                map.merge(word, 1, (prev, one) -> prev + one)
        );
        System.out.println(JSON.toJSONString(map));
    }
评论 (4)