探索JDK源码-每行代码堪称教科书级别(4)HashMap.java(下)

本文深入探讨HashMap的高级方法,如computeIfAbsent、computeIfPresent和compute,以及扩展方法forEach,解析其工作原理与应用场景,帮助开发者提升代码质量和效率。

前言

经过前两篇博客的分析,已经算是对HashMap有了较为深入的了解。既然是最后一篇讲解HashMap的博客,那必然要来分析HashMap比较不为人所知,但又相当有用的一些用法及其原理。

可以先从前两篇讲解开始了解HashMap哦:

探索JDK源码-每行代码堪称教科书级别(2)HashMap.java(上)

探索JDK源码-每行代码堪称教科书级别(3)HashMap.java(中)

高级方法

computeIfAbsent

简单理解就是从map中查找给定key的value并返回,如果找不到或者null的话,使用mappingFunction定义的返回值进行返回,并将这个返回值和key存到map中。

public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
    if (mappingFunction == null)
        throw new NullPointerException();
    int hash = hash(key);
    ... // 初始化
    if ((first = tab[i = (n - 1) & hash]) != null) {
        ... // 查找存在的value
        V oldValue;
        if (old != null && (oldValue = old.value) != null) {
            afterNodeAccess(old);
            return oldValue;
        }
    }
    V v = mappingFunction.apply(key);
    ... // 将新的value放入map中
    return v;
}

如果原来不存在key的话,会有几率触发整个map的结构调整,整体的流程相当于第二篇讲解中的最佳实践:

List<String> list = map.get(key);
if (list == null) {
    list = new ArrayList();
    map.put(key, list);
}
list.add(value);

所以从现在开始我们可以这样处理最佳实践

map.computeIfAbsent(key, k -> new List<String>()).add(value);

通过这个方法大大减少了代码量,而且不会影响执行效率。

computeIfPresent

简单理解就是从map中查找给定key的value,只有存在且不为null时才执行remappingFunction方法,并把方法的返回值刷新到map中;否则返回空。

public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V>  remappingFunction) {
    if (remappingFunction == null)
        throw new NullPointerException();
    Node<K,V> e; V oldValue;
    int hash = hash(key);
    // key必须存在map中,并且存在的value不能为空
    if ((e = getNode(hash, key)) != null &&
        (oldValue = e.value) != null) {
        // 执行额外的方法并返回新的value
        V v = remappingFunction.apply(key, oldValue);
        if (v != null) {
            // 更新原来的value
            e.value = v;
            afterNodeAccess(e);
            return v;
        }
        else
            removeNode(hash, key, null, false, true);
    }
    return null;
}

所以说如果是想在key-value都已经存在的情况下进行操作的话,可以尝试该方法

HashMap<String, List<String>> map = new HashMap<>();
map.computeIfPresent("key", (key, list) -> {
    list.add("new value");
    return list;
});

深入思考会发现,由于只会取出存在的value并刷新value,不会有增加key-value的情况,所以也就没必要对整个map刷新结构了。

compute

简单理解就是从map中获取指定key的value,不管存不存在都会执行remappingFunction方法,并把方法的返回值刷新到map中。

public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    if (remappingFunction == null)
        throw new NullPointerException();
    int hash = hash(key);
    ... // 初始化
    
    ... // 查找存在的value
    V oldValue = (old == null) ? null : old.value;
    // 执行额外的方法,刷新value
    V v = remappingFunction.apply(key, oldValue);
    if (old != null) {
        if (v != null) {
            old.value = v;
            afterNodeAccess(old);
        }
        else
            removeNode(hash, key, null, false, true);
    }
    else if (v != null) {
        // 有可能触发map结构调整
        if (t != null)
            t.putTreeVal(this, tab, hash, key, v);
        else {
            tab[i] = newNode(hash, key, v, first);
            if (binCount >= TREEIFY_THRESHOLD - 1)
                treeifyBin(tab, hash);
        }
        ++modCount;
        ++size;
        afterNodeInsertion(true);
    }
    return v;
}

在实践该方法时需要对apply方法的value判断是否为空,防止空指针异常哦。

扩展方法

forEach

该方法可以很方便的遍历整个map数据。对于遍历的每一对key-value都会通过action的apply方法进行进一步的处理,所以我们可以在apply中做自己的逻辑。

public void forEach(BiConsumer<? super K, ? super V> action) {
    Node<K,V>[] tab;
    if (action == null)
        throw new NullPointerException();
    if (size > 0 && (tab = table) != null) {
        int mc = modCount;
        for (int i = 0; i < tab.length; ++i) {
            for (Node<K,V> e = tab[i]; e != null; e = e.next)
                action.accept(e.key, e.value);
        }
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }
}

简单的实践:

map.forEach((key, value) -> System.out.println(key + " : " + value));

其实也可以通过KeySet的迭代器进行遍历,但是个人不喜欢这样,因为是在做无用功,一两行代码就能完成的遍历,没必要写一堆代码。

不过特定条件下是有必要使用KeySet的,因为forEach中这块代码决定了这个方法在多线程中执行是有风险的。也就是说一个线程在遍历,另一个线程对map做了一个put或者remove之类会影响modCount的操作,导致modCountmc不一致,代码会认为这次遍历的数据是不可靠的,然后抛出异常。

if (modCount != mc)
    throw new ConcurrentModificationException();

保险起见,我们可以使用KeySet来解决这个多线程问题:

Set<String> keys = new HashSet<>(map.keySet());
keys.forEach(key-> System.out.println(map.get(key)));

这里我们使用map.keySet获取到的当前map中存在的所有key,并复制到我们的keys中,之所以这么做,是确保接下来的遍历不会直接引用到map中的keySet,从而避免了遍历异常。

接着就是对keys的遍历了,因为这里的keys是属于当前线程的,不会出现被其它线程修改的风险,所以可以直接forEach

总结

总共三篇HashMap的源码解读,先从基础知识了解HashMap底层最为重要的一些原理,最重要的就是哈希冲突了,并如何解决哈希冲突。接着了解了HashMap中最常见的一些方法,其中的重点和难点就是Node链表和TreeNode红黑树的学习理解了。最后是认识一些开发中最为常见的提升代码质量和效率的方法,并知道怎么处理HashMap的遍历和多线程可能带来的异常。

不管读者还是处于什么阶段,~的原理的使用迟早都是要面对的,希望大家看完这三篇源码解读能够有所收获吧。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值