JDK17中HashMap的put的源码了解

本文详细解析了HashMap内部数据结构(数组和链表/红黑树)、哈希函数、put方法、resize方法以及扩容策略,揭示了HashMap在处理哈希冲突和性能优化中的关键细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.看源码之前需要了解的一些内容

Node<K,V>[] table   // 哈希表结构中数组的名字

DEFAULT_INITIAL_CAPACITY   // 数组默认长度16

MAXINUM_CAOACITY // 数组理论最大容量上限,为2的30次方

DEFAULT_LOAD_FACTOR        // 默认加载因子0.75

TREEIFY_THRESHOLD         // HashMap链表转为红黑树的最小树形化阈值

MIN_TREEIFY_CAPACITY    // 允许HashMap的某些链表转为红黑树的最小数组长度阈值

table数组是HashMap底层数据结构中的“数组”,存储链表头或红黑树的root节点 

源码中HashMap的构造方法都只是初始化了一下加载因子(loadFactor),没有对底层的数据结构(数组)进行初始化(起码table都没有分配内存) ,从后面put->putVal->resize方法可以得知:在第一次put元素时,才对HashMap对象的数组进行初始化!

HashMap里面每一个对象包含以下内容:
1.1 链表中的键值对对象
    包含:  
            int hash;         //键的哈希值
            final K key;      //键
            V value;          //值
            Node<K,V> next;   //下一个节点的地址值
            


            
1.2 红黑树中的键值对对象
    包含:
            int hash;                 //键的哈希值
            final K key;              //键
            V value;                  //值
            TreeNode<K,V> parent;      //父节点的地址值
            TreeNode<K,V> left;        //左子节点的地址值
            TreeNode<K,V> right;    //右子节点的地址值
            boolean red;            //节点的颜色

执行HashMap的put方法时:

//参数一:键
//参数二:值

//返回值:被覆盖元素的值,如果没有覆盖,返回null
//参数一:键的哈希值
//参数二:键
//参数三:值
//参数四:如果键重复了是否保留
//           true,表示老元素的值保留,不会覆盖
//           false,表示老元素的值不保留,会进行覆盖
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

hash方法

//利用键计算出对应的哈希值,再把哈希值进行一些额外的处理
//简单理解:返回值就是返回键的哈希值
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

putVal方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
		//定义局部变量:Node数组tab,Node对象p,整形n和i
        Node<K,V>[] tab; Node<K,V> p; int n, i;
		/**
		* if判断的同时将table赋值给tab,将table的长度赋值给n,这里的table是HashMap的成员变量,表示底层数据结构中的!数组!
		* 所以这里是询问是否是首次向hashMap集合中添加元素,是的话就参加扩容方法——resize
		*/
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length; // resize是扩容方法,局部变量n记录下扩容后的数组容量
		/**
		*这里的if也在边赋值边判断
		*i = (n - 1) & hash 就是JDK17的解决哈希碰撞的策略,是用(数组长度 - 1)去同 节点Key的hash值做 & 运算
		*所以JDK17不是hash值同数组长度取余的策略
		*/
        if ((p = tab[i = (n - 1) & hash]) == null) // 如果要插入节点的下标没有被插入过,就在这里插入
            tab[i] = newNode(hash, key, value, null);
        else { // 否则
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
				// 如果待插入节点的hash值与当前数组节点的hash相同,并且Key也相同
                e = p;
            else if (p instanceof TreeNode) // 如果是红黑树,那就要去红黑树的插入方法了,太难了先不看了
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
				//如果从数组中获取出来的键值对不是红黑树中的节点
				//表示此时下面挂的是链表
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
			//如果e为null,表示当前不需要覆盖任何元素
			//如果e不为null,表示当前的键是一样的,值会被覆盖
			//这里能看出,HashMap的覆盖策略不是直接替换节点而是覆盖Value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
		// threshold:记录的就是数组的长度 * 0.75,哈希表的扩容时机  16 * 0.75 = 12
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

resize方法:

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table; // 定义局部变量指向table数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length; // oldCap表示旧数组容量(length不是包含元素的数量,是new的那个值或上一次扩容的值)
        int oldThr = threshold; // 旧阈值,这里的threshold是门槛,数组扩容的门槛
        int newCap, newThr = 0; // 新数组容量,新数组扩容门槛
		// 数组如果从未扩容过(这里其实就是数组连初始化都没有),oldCap = oldThr = 0
        if (oldCap > 0) { 
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
			// 很显然,如果第一次执行put方法,前两个if分支都走不了,会到这里
            newCap = DEFAULT_INITIAL_CAPACITY; // 数组初始化容量为16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 数组初始扩容门槛为16 * 0.75 = 12
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr; // 成员变量“扩容门槛”更新
		// 下面的工作就是扩容了
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 定义一个空的容量为新容量的Node数组
        table = newTab; // table指向新数组
		// 下面的操作就是在将老数组上的链表和红黑树复制到新数组上,太复杂了不看了
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值