Java高级之HashMap中的put()方法和putIfAbsent()方法

概述

put()方法和putIfAbsent()方法:

  • 共同点
    • 都是添加键值对到HashMap中。
    • 如果以前没有添加过相同的键,则put()和putIfAbsent()方法都返回的是null,get()方法返回的都是该键对应的键值。
  • 不同点
    • 如果以前有添加过相同的键,则put()方法会用新值替换旧值,返回的是旧值;而putIfAbsent()方法不会用新值替换旧值,因此该键值对不变,返回旧值

示例代码

public class Demo {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();

        System.out.println(map.put("abc", "123"));// 在原HashMap中没有存储键为"abc"的键值对,那么get()方法得到的就是null,没有旧值
        System.out.println(map.get("abc"));// 返回键为"abc"对应的值"123"

        System.out.println();

        System.out.println(map.put("abc", "456"));// 在HashMap中已经存在键为"abc"的键值对了,用新值覆盖旧值,返回旧值
        System.out.println(map.get("abc"));// 返回键为"abc"对应的键值"456",这是被覆盖后的新值

        System.out.println();

        System.out.println(map.putIfAbsent("abc", "789"));// 在HashMap中已经存在键为"abc"的键值对,使用putIfAbsent()方法,不会覆盖旧值,返回旧值
        System.out.println(map.get("abc"));// 返回键为"abc"对应的键值"456",由于没有进行覆盖,所以返回的还是旧值

        System.out.println();

        System.out.println(map.putIfAbsent("efg", "789"));// 在HashMap中不存在具有相同键的情况,因此返回null
        System.out.println(map.get("efg"));// 返回键为"efg"对应的键值"789"
    }
}
/**
 * 打印结果:
 * null
 * 123
 *
 * 123
 * 456
 *
 * 456
 * 456
 *
 * null
 * 789
 */

源码剖析

在HashMap中查看put()和putIfAbsent()方法的源码如下:

发现两个方法的源码,几乎一样,都是调用的putVal()方法,但有一个参数不一样,那么就是onlyIfAbsent参数。

在put()方法中onlyIfAbsent参数的值是false,在putIfAbsent()方法中onlyIfAbsent参数的值是true。

那么在putVal()方法中发生了什么呢?该onlyIfAbsent参数到底在哪里起的作用,查看putVal()方法的源码:

    /**
     * 添加键值对到HashMap中
     *
     * @param hash         key所对应的哈希值
     * @param key          键值对中的键(key)
     * @param value        键值对中的值(value)
     * @param onlyIfAbsent 如果存在相同的值,是否替换已有的值,true表示替换,false表示不替换
     * @param evict        表是否在创建模式,如果为false,则表是在创建模式
     * @return 返回旧值或者null
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        HashMap.Node<K, V>[] tab;// 临时变量,用来临时存放哈希桶数组
        HashMap.Node<K, V> p;
        int n, i;
        // 检查链表数组table是否为空,table为null或者table数组的长度为0都表示为空
        if ((tab = table) == null || (n = tab.length) == 0)
            // 如果为空则初始化,并扩容,然后返回新链表数组的长度,将长度赋值给变量n
            // resize()方法就是初始化并扩容,该方法具体请参考:
            n = (tab = resize()).length;
        // (n-1)&hash这条语句就是JDK1.7中HashMap源码中的indexFor()方法的功能,即得到该对象存放在数组中的具体位置(下标)
        // 判断该位置的元素是否为null,即是否存在元素,如果存在则表示已经发生哈希冲突,如果不存在,则添加元素结点
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 表示不存在元素的情况,不发生哈希冲突
            // 则新添加一个元素到链表数组的对应下标位置,该结点也是链表的链头
            tab[i] = newNode(hash, key, value, null);
        else {
            // 表示存在元素的情况
            // 则发生了哈希冲突,下面的代码则是尝试解决冲突问题
            HashMap.Node<K, V> e;
            K k;
            // 判断待添加元素的hash值和key值是否同已经存在(冲突)的元素的hash值和key值同时相等
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                // 如果相等,则表示两个元素相互重复了,那么使用变量e来临时存储这个重复元素
                e = p;
                // 如果不相等,表示没有重复,并且判断结点类型是否是红黑树类型
            else if (p instanceof HashMap.TreeNode)
                // 那么就将该键值对存储到红黑树中
                e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
                // 如果不相等,且结点类型不是红黑树类型,那么就是链表,即采用拉链法解决冲突
            else {
                // 遍历链表中所有结点,这是一个死循环,需要通过break跳出循环
                for (int binCount = 0; ; ++binCount) {
                    // 如果p的下一个结点为null,则p是链表中的最后一个结点
                    if ((e = p.next) == null) {
                        // 则将键值对添加到最后一个结点的后面
                        p.next = newNode(hash, key, value, null);
                        // 同时binCount也是一个计数器,统计该链表已经有几个元素了
                        // TREEIFY_THRESHOLD是常量,表示阈值,默认值为8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 但链表中元素个数超过了阈值,则将链表转换成红黑树
                            treeifyBin(tab, hash);
                        // 跳出循环
                        break;
                    }
                    // 判断待添加元素的hash值和key值是否同链表中已有元素的hash值和key值同时相等
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        // 如果相等,则表示已经存在相同的键,跳出循环
                        break;
                    // 将下一个节点赋值给当前节点,继续往下遍历链表
                    p = e;
                }
            }
            // 如果e不为空,则表示已经存在重复的值,即存在hash值和key值同时相等的元素
            if (e != null) {
                // 保存旧值
                V oldValue = e.value;
                // 然后替换为新值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // 此函数会将链表中最近使用的Node节点放到链表末端,因为未使用的节点下次使用的概率较低
                afterNodeAccess(e);
                // 返回旧值
                return oldValue;
            }
        }
        // 记录修改次数
        ++modCount;
        // 如果添加元素后,超过阈值
        if (++size > threshold)
            // 则对HashMap进行扩容
            resize();
        // 给LinkedHashMap使用
        afterNodeInsertion(evict);
        return null;
    }

而onlyIfAbsent参数的使用在此处:

!onlyIfAbsent,在put()方法中onlyIfAbsent参数的值是false,那么!onlyIfAbsent得到的值就是true,所以会新值替换旧值。

在putIfAbsent()方法中onlyIfAbsent参数的值是true,那么!onlyIfAbsent得到的值就是false,所以新值不会替换旧值,即if下的语句不会得到执行。

### PutIfAbsent 方法详解 #### 使用场景 `putIfAbsent` 是 Java `Map` 接口中定义的一个方法,适用于希望向集合中插入键值对但前提是该键尚无对应值的情况。这有助于避免意外覆盖已存在的数据,在并发环境中尤其有用,可以有效减少竞态条件的发生概率[^2]。 #### 实现逻辑 此方法接收两个参数——键(`K`)与待插入的值(`V`)。它会检查给定的键是否已经存在于当前映射结构内;如果不存在,则将新值同该键关联起来并返回原先可能存在的旧值(此时应为null),反之则保持原样不变只给出既有的映射结果而不做任何更改。 ```java // 示例代码展示如何使用 putIfAbsent 方法 import java.util.concurrent.ConcurrentHashMap; public class Example { public static void main(String[] args) { ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(); // 尝试放入新的键值对 "key1" -> "value1" System.out.println(map.putIfAbsent("key1", "value1")); // 输出: null // 再次尝试放置相同键的不同值 System.out.println(map.putIfAbsent("key1", "newValue")); // 输出: value1 // 打印最终的地图状态 System.out.println(map); // {key1=value1} } } ``` 上述例子展示了当试图用不同的值替换已有键所指向的数据时,`putIfAbsent` 的行为表现。第一次调用成功添加了一个条目,并因为之前没有对应的记录所以返回了 `null`; 而第二次由于目标键已经有绑定的对象存在因而未作变动,直接反馈出了原有的那个值。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值