
11

详解HashMap源码解析(下) - 小码code
source link: https://www.cnblogs.com/jeremylai7/p/16445127.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

详解HashMap源码解析(下)
上文详解HashMap源码解析(上)介绍了
HashMap
整体介绍了一下数据结构,主要属性字段,获取数组的索引下标,以及几个构造方法。本文重点讲解元素的添加
、查找
、扩容
等主要方法。
put(K key, V value)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
首先算出key的哈希码,调用hash
方法,获取到hash
值。
- 调用putVal()
/**
* @param hash hash for key hash 值
* @param key the key key 值
* @param value the value to put value 值
* @param onlyIfAbsent if true, don't change existing value 只有不存在,才不改变他的值
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none 返回上一个值,如果不存在返回null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 声明一个node数组 tab,node 节点
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果 table 为 null 或者 tab的长度为 0 ,|| 两边都要做一下判断,table 为空,或者table的长度为0
if ((tab = table) == null || (n = tab.length) == 0)
// table 初始化
n = (tab = resize()).length;
// 不存在,直接新建一个Node节点
if ((p = tab[i = (n - 1) & hash]) == null)
// 新建节点
tab[i] = newNode(hash, key, value, null);
else {
// 存在节点
Node<K,V> e; K k;
// hash值 和 p 节点的hash值一致,(键值的地址)一致或者(键的值)一致,直接替换
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,因为从零开始所以要减一
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 转成红黑树
treeifyBin(tab, hash);
break;
}
// hash一致 或者 值一致
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// e不为空,直接替换赋值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
// 原来的值为空,赋值
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
- 首先判断哈希数组
table
是否为null
,如果为null
,就扩容。 (n - 1) & hash
对应的下标是否存在节点。- 不存在节点,就创建新的节点并赋值。
- 存在节点
- 节点key值是否相等,相等就替换
value
。 - 是否为红黑树,添加数据到红黑树中。
- 上面都不符合,就是普通链表,遍历链表,如果链表存在相同
key
就替换,否则在链表最后添加数据。
- 节点key值是否相等,相等就替换
流程图:

putAll(Map<? extends K, ? extends V> m)
putAll
是将集合元素全部添加到HashMap
中,putAll
调用了putMapEntries
方法,putMapEntries
先判断是否需要扩容,然后遍历元素,调用putVal
添加元素,下面是添加元素代码:
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
get(Object key)
通过key
找到哈希表的中Node
节点的value
值。
// 返回map映射对应的value值
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
首先使用hash
方法算出哈希值,然后再调用getNode()
获取数据:
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 判断tab有数据,并且对应下标存在数据
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// hash相等以及key相等(key地址相等或者key的值相等),找的就是第一个元素
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 遍历链表
if ((e = first.next) != null) {
// 红黑树找到当前key所在的节点位置
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 普通链表,往后遍历,直到找到数据或者遍历到链表末尾为止
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
- 判断哈希数组是否不为
null
并且数组下标(n - 1) & hash
处不为null
,如果都有值,就查询首节点first
,否则返回null
。 - 找到首节点,匹配上相等的
hash
和key
,返回首节点。 - 链表有多个元素,是否为红黑树
- 是红黑树,在红黑树查找
- 不是红黑树,就遍历普通链表,直到匹配到相同的
hash
和key
值。
流程图:

resize 扩容
当哈希数组为null
,或元素个数超过了阈值,就调用resize
扩容方法:
final Node<K,V>[] resize() {
// 记录原数组
Node<K,V>[] oldTab = table;
// 原数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 原阈值(数组长度达到阈值)
int oldThr = threshold;
// 新容量,新阈值
int newCap, newThr = 0;
if (oldCap > 0) {
// 数组长度大于或者等于MAXIMUM_CAPACITY(1>>30)不做扩容操作。
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 扩容后长度小于MAXIMUM_CAPACITY(1>>30)并且数组原来长度大于16
// 阈值和新容量都翻倍
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
// oldCap 和 oldThr 都小于等于0,说明是调用无参构造方法,赋值默认容量16,默认阈值12。
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
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数组。调用无参构造方法,并不会创建数组,在第一次调用put方法,才会调用resize方法,才会创建数组,延迟加载,提高效率。
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 原来的数组不为空,把原来的数组的元素重新分配到新的数组中
// 如果是第一次调用resize方法,就不需要重新分配数组。
if (oldTab != null) {
// 旧数组遍历
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 存在下标下的第一个元素
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 当前元素下一个元素为空,说明此处只有一个元素,直接使用元素的hash值和新数组的容量取模,获得新下标的位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 红黑树,拆分红黑树,必要时可能退化为链表
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 长度大于1的普通链表
else { // preserve order
// loHead、loTail分别代表旧位置的头尾节点
Node<K,V> loHead = null, loTail = null;
// hiHead、hiTail分别代表新位置的头尾节点
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 遍历链表
do {
next = e.next;
// & 与运算,两个都会1,结果才为1
// 元素的hash值和oldCap与运算为0,原位置不变
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 移动到原来位置 + oldCap
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;
}
- 原容量是否为空
- 不为空,是否大于最大容量
- 大于最大容量,不做扩容
- 小于最大容量,并且大于默认容量16。阈值和容量都翻倍。
- 为空,原阈值大于零, 就阈值赋值给新容量。
- 不为空,是否大于最大容量
- 原容量和原阈值都小于等于零,赋值默认容量16和默认阈值12。
- 做完阈值和容量的赋值之后,遍历数组。
- 有值,是否只有一个元素,如果是就放入新数组
n-1&hash
下标处。 - 如果是红黑树就拆分红黑树。
- 上面两个都不符合就是普通链表。
- 遍历链表,如果
hash&数组原长度
为0- 放在数组
原下标
处。 - 不为零,放在
原位置+原数组长度
处。
- 放在数组
流程图:

本文主要讲解了元素的添加
、查找
、扩容
等主要方法,其中添加
和查询
都需要先获取数组的下标,然后进行对应的操作。
put
添加
- 首次添加数据需要对数组进行扩容。
- 对应下标是否有值
- 没有值,直接赋值
- 有值
key
一致,替换value
值。key
不一致- 是红黑树,在红黑树添加数据。
- 不是红黑树,就是链表,遍历链表,存在相同节点key,替换。否者添加在链表的尾部。
get
查询
- 下标是否有值
- 没有值,返回
null
- 有值
*hash
和key
相等的话,返回节点。- 是否是多链表。
- 不是,返回
null
。 - 是的话,是否是红黑树。
- 红黑树,在红黑树中查找
- 否则就是普通链表,遍历链表知道匹配到相同的
hash
和key
。
- 不是,返回
- 是否是多链表。
- 没有值,返回
resize 扩容
- 容量大于零
- 大于最大容量值,不再扩容。
- 介于最大和默认容量之间,阈值和容量都翻倍。
- 初始化的时候,设置默认容量和默认阈值。
- 遍历原数组
- 节点有值,并且只有一个值,赋值给新数组
n-1&hash
处。 - 如果是红黑树,就拆分红黑树。
- 以上都不符合,就是普通链表,遍历链表。因为数组长度都是2的幂次方,扩容后元素的位置*要么是在原位置,要么是在原位置再移动2次幂的位置。
- hash&与运算原数组长度,等于0,存在原来的位置。
- 不等于0,就存放下标原来位置+原数组长度位置处。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK