一、HashMap计算链表索引原理
1.Hash值计算
取HashKey对象的hashCode值,高十六位维持不变 低十六位与高十六位进行异或运算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2.HashTab数组计算索引
if ((p = tab[i = (n - 1) & hash]) == null)
这里的n在HashMap中一定是2的n次幂 满足这个条件下
(n - 1) & hash 等价于 hash % n
这里给出推导,因为位运算中 >> 1相当于/2 而因左移而丢失的部分则是余数
如 1111 1001 这个二进制为249 左移动4位 相当于 / 2的4次方(16) 1111 1101 >> 4 = 1111 即15
那么余数恰好就是左移后丢失的1001,反推1111 << 4 = 1111 0000 此时相当于 15 * 16 = 240 这时候需要加上余数1001(十进制9)才刚好等于1111 1001(十进制249)。而想要获取到这后4位(余数)则需要4位 1111 与hash进行与运算,而1111则恰好是16 - 1 。这也要求了n必须是2的次方幂。
PS: 至于为何上文中采用h = key.hashCode()) ^ (h >>> 16) 计算 hash值
原因是 (n - 1) & hash 中 (n-1)只有低位的几个位,而高位则不会参与到 与 运算中,为了减少hash冲突,则将高位移至低位,与低位进行异或运算,使hash运算中高低位都能参与到计算中来,减少hash冲突的可能性。使得散列的更均衡。
二、 HashMap 存储
1.HashMap存储结构
HashMap的存储以 数组 + (链表 | 红黑树) 数组 + 链表 结构存储具体如下,node数组存储链表头节点,节点之间根据成员对象next引用形成链表
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
2.链表转红黑树
通过hash运算定位到数组对应索引上,若存在hash冲突则以链表形式保存起来。
若新增元素后(put方法)后在链表长度达到8(并且map总容量达到64)时链表转换为红黑树存储
3.扩容
HashMap 指定容量时 若设定的容量不是2的n次幂则向上取整到最近的2的n次幂,若不设置则取默认16为长度
HashMap存在一个参数 负载因子默认为0.75,代表若HashMap中实际存储的数据量达到了容量 * 0.75则进行扩容,扩容到当前的两倍。
扩容时,若存储的节点为链表节点,则进行位运算, 旧容量 & hash 因为容量是2的n次幂,则只有一个二进制位为1,做旧容量 & hash == 0 将链表分为两条为链表,分别是高位链表、低位链表,低位链表在原位置,而高位链表在新数组则在原位置 + 旧容量的位置,原因是因为 (新容量 - 1) 和 原来的 (旧容量 - 1)之间只是多了一个1,即若原来容量为 8 则 8 - 1 = 0000 1111 新容量为 16 则 16-1 = 0001 1111 即只有从左到右的第四位不同,多的这一位 0001 0000 则就是旧容量的二进制,hash值与这个数相与,若为0则代表 (新容量 - 1) & hash和(旧容量 -1) & hash是没区别的,因此不变,而若旧容量 & hash != 0 则代表这个位在新的容量下计算的值为1,即原位置 + 旧容量就是新位置的索引。
4.红黑树转链表
红黑树在几个场景下会转换回链表
第一种情况,就是扩容造成的红黑树节点减少,此时若扩容后红黑树节点小于等于6个则退化为链表。
第二种情况,则无关红黑树节点数量,调用remove时,检查红黑树状态(这里指红黑树没做移除动作前),若根节点的左孩子或者右孩子或者左孩子的左孩子(左孙子)为null,这时候需要将红黑树转换为链表。
5.put和putAll扩容区别
put方法先插入再扩容
putAll先扩容再插入
三、题外小知识
1.旧版本HashMap死循环问题
在jdk1.7版本下HashMap插入链表是头插法插入链表(jdk1.8采用尾插法),在并发情况下,触发扩容,则可能会造成链表引用关系成环,造成死循环。