本文案例Java版本是JDK-17
一、Put源码及流程
Map<String, String> map = new HashMap<>();
map.put("a", "1");
进入put()方法,如下图:
hash(key):计算key的hash值,非简单key.hash(),具体下面讲解;
onlyIfAbsent:为true时,不更新已有数据。相当于:putIfAbsent(key, value),如下图:
evict: 由子类实现,HashMap未实现该方法。
1,hash(key)源码及流程
1.1,计算key.hashCode()并复制给int变量h;
1.2,将h右移16位。int类型占4个字节,共32位。此时将h右移16为,相当于去掉了低位的16位,高16位将补0.
1.3,将原h与右移后的h做 ^(与运算)。与运算:有0为0,全1为1.
这么做的好处是,简单的hashCode()方法,高位多数都为0,更容易出现hash碰撞,右移16位后再与运算,相当于高位和地位进行运算,使用了高位,降低了hash碰撞。
2,putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)源码及流程
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 分支一:通过无参构造函数初始化map时,table为空
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 分支二:计算下标,并判断数组下标处为空,不存在节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 分支三:已有节点,即hash碰撞处理
else {
Node<K,V> e; K k;
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);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = 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;
}
分支一:通过无参构造函数初始化map时,table为空。
初始化map共有四个:
2.1.1,无参构造函数
// static final float DEFAULT_LOAD_FACTOR = 0.75f; 默认的负载因子
// final float loadFactor; 定义的负载因子变量
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
2.1.2,指定初始化大小
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
指定初始化大小的构造函数,调用了下方参数为“初始化大小”、“负载因子”构造函数
2.1.3,指定初始化大小和负载因子
// static final int MAXIMUM_CAPACITY = 1 << 30; int最大值
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
// 参数负载因子复制给变量
this.loadFactor = loadFactor;
// 根据传入的初始化大小扩容
this.threshold = tableSizeFor(initialCapacity);
}
可知,在传入初始化大小参数时,hashMap并未直接使用参数值作为初始容量,而是根据参数计算了实际的map容量。即:tableSizeFor(initialCapacity);该方法源码如下:
static final int tableSizeFor(int cap) {
// 无符号右移在高位补0,故下面一行代码表示:通过右移,将参数的高位0全部去除
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
// 去除高位0后,只剩下低位的1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
// 计算指定整数在二进制表示中最高位1之前0的数量。比如:数字8,二进制如下:
// 00000000 00000000 00000000 00001000 调用该方法后,返回28,即:1前面有28个0
public static int numberOfLeadingZeros(int i) {
// 如果参数<=0,设置初始长度为32或0
if (i <= 0)
return i == 0 ? 32 : 0;
int n = 31;
// 1 << 16:1左移16位,即 1 乘以 2 的 16 次方,等于 65536,二进制为:
// 00000000 00000000 00000000 00000001 00000000 00000000 00000000 00000000
// n 减去16,i 再无符号右移16位
if (i >= 1 << 16) { n -= 16; i >>>= 16; }
// 1 << 8:256
if (i >= 1 << 8) { n -= 8; i >>>= 8; }
// 16
if (i >= 1 << 4) { n -= 4; i >>>= 4; }
// 4
if (i >= 1 << 2) { n -= 2; i >>>= 2; }
return n - (i >>> 1);
}
由上述代码注释可知,tableSizeFor方法的作用是:返回大于或等于cap的最小2次幂值。比如:cap=10,则tableSizeFor(10)=16 ;cap=20,则tableSizeFor(20)=32;cap=32,则tableSizeFor(32)=32。
由此可得出结论:
1,无论是否传入初始容量,HashMap的初始容量必定是2的n次幂;
2,当传入初始容量,实际容量为大于或等于入参的最小的2的幂值;
2.1.4,根据已有Map新建Map(深拷贝)
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 遍历参数map,元素放入新的map中
putMapEntries(m, false);
}
由上述四个构造函数可知,不管使用哪个,在map第一次Put值时,table都为空。即第一次往map中填值时,都会走进分支一,即如下源码:
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
resize()为扩容方法,第二部分将详述。此处采用无参构造方法初始化map,默认长度为:1 << 4 = 16,负载因子为:0.75f。
Node<K,V>[] tab:声明一个初始Node数组。map中存放的结点就是Node对象;
Node<K,V> p:声明的临时节点变量;
n:节点数组长度;
i:声明的临时数组下标;
分支二:计算下标,并判断数组下标处无节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
上面代码可知,计算下标的方法为:(n-1)&hash.
n-1直接表明了数组边界,比如n=16,则n-1=15,二进制 1111,与hash值做与运算,结果必然是【0,15】。
当该下标处节点为空时,新建Node对象放入该下标。不走分支三,接着判断长度是否超过阈值:16*0.75=12,没超过则返回null,超过则扩容。
分支三:hash碰撞处理
else {
Node<K,V> e; K k;
// 如果hash碰撞且key相同,则直接覆盖原key值
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 发生hash碰撞且key不同,判断是否是红黑树。如果是红黑树,在红黑树插入节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 发生hash碰撞且key不同且不是红黑树,表明是链表
else {
for (int binCount = 0; ; ++binCount) {
// p在分支二赋的值,将p的next赋值给e,并判断是否为空。
if ((e = p.next) == null) {
// 如果p.next为空,表示该下标只有一个节点。则新增结点放入尾部,即尾插法。
p.next = newNode(hash, key, value, null);
// 判断此时的链表长度,如果大于7,默认为8,则转换链表为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 覆盖原值的双重判断
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
// 如果该下标不止一个节点,则重新赋值p接着循环,相当于在遍历链表。直至p.next为空
p = e;
}
}
// 再次判断原值覆盖,根据参数onlyIfAbsent
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
二,扩容源码及流程
扩容代码融合较多逻辑,可按照注释步骤阅读。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 步骤四:非初始化,非第一次Put。常规扩容时,oldCap必然>0
if (oldCap > 0) {
// 防小人判断,防止容量超过最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 旧容量左移一位,相当与乘以2,赋值给newCap。这里多了一层扩容阈值处理,
// 如果oldCap大于默认容量16,才将扩容阈值乘以2,否则就按照步骤三计算
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 步骤二:使用给定大小的构造函数声明map,初始化时重新计算了实际大小:大于入参的最小2次幂值
// 复制实际大小给局部变量newCap
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 步骤一:无参构造函数声明的map,使用默认大小和默认负载因子和默认阈值
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 步骤三:非无参构造函数,扩容阈值需要根据计算出来的newCap计算
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 步骤五:根据newCap声明Node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 步骤六:常规扩容时,需要将旧数组的值重新放入新的数组,这个方法后面单独讲解。
if (oldTab != null) {
// oldTab[oldCap] -> newTab[newCap]
for (int j = 0; j < oldCap; ++j){...}
}
return newTab;
}
上述代码中的步骤一到步骤四,主要功能就是计算数组容量和扩容阈值。
- 使用无参构造函数声明HashMap时,使用默认值;
- 常规扩容时,通过oldCap<<1,使得新数组大小为旧数组大小的两倍。即双倍扩容
三,扩容后旧数组数据移至新数组
根据二所述,常规扩容时迁移数据,oldTab !=null 为true,只需看if 判断的for循环
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// if ((e = oldTab[j]) != null)作用:旧数组该下标没有节点,则跳过不处理
// 如果该下标不为空,将该节点赋值给e
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// e.next==null,表示该下标只有一个节点
if (e.next == null)
// 直接计算新下标,并将该节点放至新数组下标
newTab[e.hash & (newCap - 1)] = e;
// hash冲突时,判断该索引节点的类型,是否是树节点
else if (e instanceof TreeNode)
// 拆分树节点,将拆出来的节点重新计算下标并放入newTab中
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 非单个节点,非树节点,表明该下标是链表
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;
// e.hash&oldCap与重新计算下边的方式(e.hash&(newCap-1))类同,表明:
// 新的下标要么在[原位],要么在[原位+旧容量]的位置。
// 此处判断==0为true时表示在原位,else表示在高位
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);// 通过while遍历链表
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
扩容重新计算位置的特点:
新数组下标,要么在旧数组原下标位,要么在【旧数组原下标位+旧数组容量】位