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