本文已参与「新人创作礼」活动,一起开启掘金创作之路。
HashMap
HashMap JDK1.7 数据结构图
编辑
HashMap JDK1.8 数据结构图
编辑
HashMap JDK1.8 put 数据流程图 (泳道图)
编辑
HashMap源码解释
HashMap类
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
}
1.单继承自AbstractMap,多实现Map,Cloneable..
HashMap成员变量
//默认初始容量为 16= 位运算2的4次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量 2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认扩容加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表转红黑树的阈值 等于9的时候才考虑是否转红黑树
static final int TREEIFY_THRESHOLD = 8;
//红黑树转链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//可以树形化的容器的最小表容量。JDK1.8链表转红黑树时候的容量前提条件
static final int MIN_TREEIFY_CAPACITY = 64;
-
默认初始容量为 16= 位运算2的4次方,不直接写16是因为位运算的效率更高。
-
默认扩容加载因子是0.75,是因为考虑空间时间折中数字,有网友用牛顿二项式计算出适合 扩容的因子是0.6913
-
链表转红黑树的阈值设置为8是因为波尔分布概率,当链表长度为8时的概率为
0.00000006极低的概率,是考虑海量数据或人为干扰,变换数据结构为红黑树,此时提升性能的体现是海量数据。 -
JDK1.8链表转红黑树时候的容量前提条件是数组的容量大于64
HashMap放入元素时计算数组下标(不变)
//返回hash
//(h = key.hashCode()) ^ (h >>> 16) 把object方法中取出的hashcode进行位或运算返回
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//如果数组的(n - 1) & hash 位置是空的 :
//n = (tab = resize()).length; n是数组长度-1
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//(n - 1) & hash hash值与(数组长度-1)进行与运算才是数组下标的值
- hashcode与16进行位或运算作为hash值
- hash值再与(数组长度-1)进行与运算 作为最终数组下标值。
-
为什么与16进行位或运算?
- 因为效率更高
-
为什么(数组长度-1)?
- 因为数组长度是2的幂次方值,任何二进制数与其或运算后都会是在数组的头或尾部。如果减一,与运算结果可以散列在0-15之间
HashMap放入重复key怎么办?(不变)
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
HashMap扩容时候怎么办?(重点变化)
JDK1.7
-
当前容量>总容量 * 0.75
- 为什么是0.75?源码中注释有介绍:是考虑到时间和空间的折中数字。
-
扩容到原容量的2倍,保证2的指数次幂。
-
移动原来元素去新数组(扩容后的新容量)。源码见transfer方法如下,会导致多种问题。
void transfer(Entry[] newTable, boolean rehash){
int newCapacity = newTable.length;
//遍历原数组,
for (Entry<K, V> e : table) {
//链表遍历
while (null != e) {
// 第一行
Entry<K, V> next = e.next;
if (rehash) {
//再次hash运算--->影响效率
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
// 第二行
e.next = newTable[i];
// 第三行
newTable[i] = e;
// 第四行
e = next;
}
}
}
问题点:
- 再次hash的效率问题。
- 移动过程中链表的先后顺序会颠倒,因为采用头插法导致。(第一行、第二行..第四行)的指针移动导致。
- 多线程情况下:某一线程执行后链表的先后顺序会颠倒,另一线程并发移动时重新构建链表后链表呈循环状态(即链表末尾元素的next指针会指向头节点而不是null),导致再头插法进新元素时遍历链表会进入死循环。
JDK1.8
- 当前容量>总容量 * 0.75 。 扩容到原容量的2倍,保证2的指数次幂。移动原来元素去新数组(扩容后的新容量)(没有改变思路)
- 【变化的】是扩容时候移动的代码,如下
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;
}
}
改进点:
-
去掉再次hash定位,效率提升。取而代之的是高地位指针【前提:数组的长度保持2的幂次方】。
-
高位Node<K,V> hiHead = null, hiTail = null;
- 如果(e.hash & 旧容量) == 0 用低位指针
-
低位Node<K,V> loHead = null, loTail = null;
- 如果(e.hash & 旧容量) != 0 用高位指针
-
经过循环后会产生两个链表,
-
低位链表移到新数组的与旧数组【同样位置】
-
高位链表移到【同样位置+旧的容量 = 新位置】
编辑
-
-
-
尾插的, 此思路有效避免循环链表问题。
-
此思路适用于分库分表在线扩容。