HashMap是如何进行put操作的

前言

在Java集合中,HashMap是 Map 接口使用频率最高的实现类,它的重要性不言而喻。HashMap采用key-value这种存储键值对的数据结构,每个key对应唯一的value,查询和修改的速度都很快,能达到O(1)的平均时间复杂度。它在日常开发中有着非常多的应用场景,也是面试中的高频考点,本篇文章就来分析一下HashMap中的put方法,分析一下HashMap的put方法的相关操作。

HashMap的数据结构

在讲解put方法之前,我们先来了解一下HashMap底层的数据结构,

JDK1.8之前——数组、链表

大方向上,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表

JDK1.8之后——数组、链表或红黑树

Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表或红黑
组成。

HashMap的put方法的具体流程

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值
对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了 大容量threshold,如果超过,进行扩容。

put方法的源码分析

/**
 * @param key  存储的key
 * @param value 存储的value
 * @return put
 */
public V put(K key, V value) {
    //hash方法实际是让key.hashCode()与key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。
	return putVal(hash(key), key, value, false, true);
}

/**
 * @param hash key的hash值
 * @param key  存储的key
 * @param value 存储的value
 * @param onlyIfAbsent 当这个为true的时候,若当前key在hashmap 中有值(不为null),则不会修改旧值
 * @param evict只有在方法 afterNodeInsertion(boolean evict) { }用到,可以看到它是一个空实现,因此不用关注这个参数
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
			   boolean evict) {
    /**
    tab : 创建临时节点表
    p : 临时节点
    n : hashMap.table 的长度
    i : 记录table的索引
    */
	Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 将hashMap的table 赋给临时变量 table , 当 tab == null 时 调用 resize()方法进行初始化
    // 当 tab !=  null 接着判断 tab的长度也不能为0 , 否则还是调用 resize() 方法进行初始化。
	//判断table是否为空,如果空的话,会先调用resize扩容
	if ((tab = table) == null || (n = tab.length) == 0)
        // 把初始化后的 table 赋给 tab , table的长度赋给 n
		n = (tab = resize()).length;
	//根据当前key的hash值找到它在数组中的下标,判断当前下标位置是否已经存在元素,
	//若没有,则把key、value包装成Node节点,直接添加到此位置。
	// (n - 1) & hash 是计算当前元素插入位置的下标,为什么这样算,后边讲
    //当这个地方的元素为null时,则说明以前没有放元素进来,这里可以直接插入
	if ((p = tab[i = (n - 1) & hash]) == null)
        // 在索引为i的位置,创建一个新节点直接放在这里即可
		tab[i] = newNode(hash, key, value, null);
	else { 
		//如果当前位置已经有元素了,分为三种情况。
		Node<K,V> e; K k;
		//1.当前位置元素的hash值等于传过来的hash,并且他们的key值也相等,
		//则把p赋值给e,跳转到①处,后续需要做值的覆盖处理
		if (p.hash == hash &&
			((k = p.key) == key || (key != null && key.equals(k))))
			e = p;
		//2.如果当前是红黑树结构,则把它加入到红黑树 
		else if (p instanceof TreeNode)
            // 如果是一个红黑树,则调用红黑树的 putTreeVal 方法
			e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
		else {
		//3.说明此位置已存在元素,并且是普通链表结构,则采用尾插法,把新节点加入到链表尾部,这里bitCount 记录的是链表长度,
			for (int binCount = 0; ; ++binCount) {
                // 如果 p.next == null ,则直接新建一个节点并让p.next 指向它
				if ((e = p.next) == null) {
					//如果头结点的下一个节点为空,则插入新节点
					p.next = newNode(hash, key, value, null);
					//如果在插入的过程中,链表长度超过了8,则转化为红黑树
					if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //调用 treeifyBin(tab, hash) 进行树化
						treeifyBin(tab, hash);
					//插入成功之后(树化后),跳出循环,跳转到①处
					break;
				}
                //这里判断,当e.hash和传入hash相同时,且他们的key也相同,则跳出循环
				//若在链表中找到了相同key的话,直接退出循环,跳转到①处
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					break;
				p = e;
			}
		}
		//①
		//说明发生了碰撞,e代表的是旧值,因此节点位置不变,但是需要替换为新值
        // 当e != null ,说明这个key对应的映射关系已经存在
		if (e != null) { // existing mapping for key
            // 取出原先的值
			V oldValue = e.value;
            //当onlyIfAbsent 为false 或  oldValue == null时,会用新值替换旧值,并返回旧值。
			if (!onlyIfAbsent || oldValue == null)
                // 把e的value设为新传入value
				e.value = value;
			//看方法名字即可知,这是在node被访问之后需要做的操作。其实此处是一个空实现,
			//只有在 LinkedHashMap才会实现,用于实现根据访问先后顺序对元素进行排序,hashmap不提供排序功能
			// Callbacks to allow LinkedHashMap post-actions
			//void afterNodeAccess(Node<K,V> p) { }
			afterNodeAccess(e);
            //返回旧值
			return oldValue;
		}
	}
	//modCount:记录hashMap修改的次数,也是fail-fast机制
	++modCount;
	//如果当前数组中的元素个数超过阈值,则扩容
	if (++size > threshold)
		resize();
	//同样的空实现
	afterNodeInsertion(evict);
	return null;
}

HashMap的put方法的总结

  1. 判断table是否为空,如果空的话,会先调用resize扩容;
  2. 根据当前key的 hash 值,通过 (n - 1) & hash计算应当存放在数组中的下标 index ;
  3. 查看 table[index] 是否存在数据,没有数据就构造一个 Node 节点存放在 table[index] 中;
  4. 存在数据,说明发生了 hash 冲突,继续判断 key 是否相等,如果相等,用新的 value 替换原数据(这里onlyIfAbsent 为 false);
  5. 如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创建树型节点插入红黑树中;
  6. 如果不是树型节点,则采用尾插法,把新节点加入到链表尾部;判断链表长度是否大于 8, 大于的话链表转换为红黑树;
  7. 插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍。

  • 4
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
HashMap的put方法是用来将键值对存储到HashMap中的。在put方法中,首先通过hash(key)计算出要存储的位置,然后调用putVal方法来实际执行插入操作。\[1\] 在putVal方法中,首先判断要插入的位置是否已经存在元素。如果该位置为空,则直接将键值对存储在该位置上。如果该位置已经存在元素,则需要根据具体情况进行处理。 如果该位置上的元素是一个链表,则需要遍历链表,查看是否存在与要插入的键相同的键。如果存在相同的键,则更新对应的值;如果不存在相同的键,则将新的键值对添加到链表的末尾。同时,还需要判断链表的长度是否达到了阈值,如果达到了阈值,则将链表转换为红黑树。\[2\] 如果该位置上的元素是一个红黑树节点,则将要插入的键值对封装成一个红黑树节点,并添加到红黑树上。在添加过程中,还会判断红黑树中是否已经存在相同的键,如果存在,则更新对应的值。\[3\] 总结起来,HashMap的put方法会根据键的哈希值计算出存储位置,然后根据具体情况进行插入操作,包括直接存储、链表插入和红黑树插入。 #### 引用[.reference_title] - *1* *2* [HashMap中put方法详解](https://blog.csdn.net/lubiaojava/article/details/119780055)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [HashMap的put方法](https://blog.csdn.net/AAAWell/article/details/125898104)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小刘要努力(ง •̀_•́)ง

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值