put方法
HashMap 只提供了 put 用于添加元素,putVal 方法只是给 put 方法调用的一个方法,并没有提供给用户使用。
对 putVal 方法添加元素的分析如下:如果定位到的数组位置没有元素 就直接插入。如果定位到的数组位置有元素就和要插入的 key 比较,如果 key 相同就直接覆盖,如果 key 不相同,就判断 p 是否是一个树节点,如果是就调用e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)
将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。
HashMap HashMap的put()方法用于向HashMap中添加键值对,当调用HashMap的put()方法时,会按照以下详细流程执行(JDK8 1.8版本):
第一步:根据要添加的键的哈希码计算在数组中的位置(索引)。
第二步:检查该位置是否为空(即没有键值对存在)
- 如果为空,则直接在该位置创建一个新的Entry对象来存储键值对。将要添加的键值对作为该Entry的键和值,并保存在数组的对应位置。将HashMap的修改次数(modCount)加1,以便在进行迭代时发现并发修改。
第三步:如果该位置已经存在其他键值对,检查该位置的第一个键值对的哈希码和键是否与要添加的键值对相同?
- 如果相同,则表示找到了相同的键,直接将新的值替换旧的值,完成更新操作。
第四步:如果第一个键值对的哈希码和键不相同,则需要遍历链表或红黑树来查找是否有相同的键:
如果键值对集合是链表结构,从链表的头部开始逐个比较键的哈希码和equals()方法,直到找到相同的键或达到链表末尾。
- 如果找到了相同的键,则使用新的值取代旧的值,即更新键对应的值。
- 如果没有找到相同的键,则将新的键值对添加到链表的头部。
如果键值对集合是红黑树结构,在红黑树中使用哈希码和equals()方法进行查找。根据键的哈希码,定位到红黑树中的某个节点,然后逐个比较键,直到找到相同的键或达到红黑树末尾。
- 如果找到了相同的键,则使用新的值取代旧的值,即更新键对应的值。
- 如果没有找到相同的键,则将新的键值对添加到红黑树中。
第五步:检查链表长度是否达到阈值(默认为8):
- 如果链表长度超过阈值,且HashMap的数组长度大于等于64,则会将链表转换为红黑树,以提高查询效率。
第六步:检查负载因子是否超过阈值(默认为0.75):
- 如果键值对的数量(size)与数组的长度的比值大于阈值,则需要进行扩容操作。
第七步:扩容操作:
- 创建一个新的两倍大小的数组。
- 将旧数组中的键值对重新计算哈希码并分配到新数组中的位置。
- 更新HashMap的数组引用和阈值参数。
第八步:完成添加操作。
此外,HashMap是非线程安全的,如果在多线程环境下使用,需要采取额外的同步措施或使用线程安全的ConcurrentHashMap。
源码如下:
java
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// table未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素(处理hash冲突)
else {
Node<K,V> e; K k;
//快速判断第一个节点table[i]的key是否与插入的key一样,若相同就直接使用插入的值p替换掉旧的值e。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
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);
// 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
// 这个方法会根据 HashMap 数组来决定是否转换为红黑树。
// 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构性修改
++modCount;
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
get方法
HashMap的get(Object key)
方法用于根据指定的键(key)获取其对应的值(value)。当调用此方法时,会执行与put()
方法类似的定位逻辑来查找节点,但过程相对简单,因为它不涉及修改操作。以下是详细的执行流程(JDK 8 1.8版本):
第一步:计算哈希值,定位数组索引
- 首先,
get(key)
方法会调用内部的getNode(hash(key), key)
方法。 - 在
getNode
方法中,它会根据传入的key
计算出其哈希码(hash value)。 - 然后,通过位运算
(n - 1) & hash
(其中n
是HashMap内部数组的长度)来精确计算出该key
应该位于数组的哪个索引位置(即哪个桶)。
第二步:检查桶内情况,优先检查第一个节点。
- 定位到数组索引后,系统会检查该位置是否存在节点。
- 如果该位置为
null
(即桶为空),则意味着Map中不存在这个key
,getNode
方法直接返回null
。 - 如果该位置不为
null
,系统会进行一个快速判断:检查该桶中第一个节点的哈希值和key
本身是否与要查找的key
完全匹配(先比较hash
,再用==
或equals()
比较key
)。 - 如果第一个节点就匹配成功,则直接返回这个节点,查找结束。这是对无哈希冲突情况的优化。
第三步:处理哈希冲突,遍历链表或红黑树。
- 如果第一个节点不匹配,并且该节点后面还存在其他节点(即
first.next != null
),则说明发生了哈希冲突,需要进一步在链表或红黑树中查找。 - 判断数据结构:
- 如果为红黑树:通过
first instanceof TreeNode
判断。如果是,则调用红黑树专属的getTreeNode()
方法,在树中进行高效查找。 - 如果为链表: 则从第二个节点开始,进入一个
do-while
循环,逐个向后遍历链表中的每个节点。
- 如果为红黑树:通过
- 遍历查找:
- 在遍历链表或查找红黑树的过程中,对每个节点都进行哈希值和
key
的比较。 - 如果在链表或红黑树中找到了完全匹配的节点,则立即返回该节点。
- 如果遍历完整个链表或红黑树仍未找到匹配项,
getNode
方法将返回null
。
- 在遍历链表或查找红黑树的过程中,对每个节点都进行哈希值和
第四步:返回最终结果。
get
方法会接收getNode
方法的返回值(一个Node
对象或null
)。- 如果返回的节点对象不为
null
,get
方法会提取并返回该节点的value
值。 - 如果返回的节点为
null
,则get
方法最终也返回null
,表示在HashMap中未找到指定的key
。
源码如下:
java
// get方法是getNode方法的封装,它处理了getNode返回null的情况
public V get(Object key) {
Node<K,V> e;
// 调用getNode获取节点,如果节点为null,则返回null,否则返回节点的value
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 实现Map.get()和相关方法
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 1. 检查table是否初始化,长度是否大于0,以及根据hash计算出的位置上是否有节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 2. 检查第一个节点,如果匹配,直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 3. 如果第一个节点不匹配,且存在后续节点
if ((e = first.next) != null) {
// 3.1 如果是红黑树,调用getTreeNode在树中查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 3.2 如果是链表,遍历链表查找
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 4. 如果table为空,或桶为空,或遍历完没找到,则返回null
return null;
}
参考链接:https://xiaolincoding.com/interview/collections.html#hashmap的put过程介绍一下