参考文章:
https://blog.csdn.net/u013494765/article/details/77837338https://blog.csdn.net/cadi2011/article/details/104851355
https://blog.csdn.net/weixin_39667787/article/details/86678215
如何确定元素在数组中的索引下标?
一共有两个步骤
1.先使用hash方法
hash方法中将key的hashcode与他的高16位进行一次亦或运算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
为什么获取key的hashcode之后还需要和hashcode右移16位进行亦或运算?
个人观点:(不保证正确)
0000 0100 1011 0011 1101 1111 1110 0001
>>> 16 (无符号右移16位)
0000 0000 0000 0000 0000 0100 1011 0011
hashcode为int类型,4个字节32位
为了确保散列性,肯定是32位都能进行散列算法计算是最好的。
首先要明白,为什么用亦或计算,二进制位计算,a 只可能为0,1,b只可能为0,1。a中0出现几率为1/2,1也是1/2,b同理。
平时用的运算符有三种,|,&,^,或,与,亦或。
a,b进行位运算,
有4种可能 00,01,10,11
a或b计算 结果为1的几率为3/4,0的几率为1/4
a与b计算 结果为0的几率为3/4,1的几率为1/4,
a亦或b计算 结果为1的几率为1/2,0的几率为1/2
所以,进行亦或计算,得到的结果肯定更为平均,不会偏向0或者偏向1,更为散列。
右移16位进行亦或计算,我将其拆分为两部分,前16位的亦或运算,和后16位的亦或运算.
后16位的亦或运算,即原hashcode后16位与原hashcode前16位进行亦或计算,得出的结果,前16位和后16位都有参与其中,保证了 32位全部进行计算。
前16位的亦或运算,即原hasecode前16位与0000 0000 0000 0000进行亦或计算,结果只与前16位hashcode有关,同时亦或计算,保证 结果为0的几率为1/2,1的几率为1/2,也是平均的。
结论:
(1)结果的后16位保证了hashcode32位全部参与计算,也保证了0,1平均,散列性
(2)结果的前16位保证hashcode前16位了0,1平均散列性,附带hashcode前16位参与计算。
(3) 16与16位数相同,利于计算,不需要补齐,移去位数数据
更多情况,hashmap只会用到前16位(临时数据一般不会这么大)个人觉得(1)占主因
2. 索引下标取值为:(n - 1) & hash
jdk1.8中,hashMap获取某个key值,通过
(n - 1) & hash
去得到索引下标,其中,n为当前map容量,hash为key通过hash方法得到的值
resize方法
//该方法用于对已存在元素的map进行扩容操作,不存在数据可以直接更改阈值就行
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) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//扩容后容量为旧容量的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//扩容后阈值为旧阈值的两倍
newThr = oldThr << 1;
}
else if (oldThr > 0) threshold
newCap = oldThr;
else {
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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//遍历哈希表的数组部分
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果当前位置没有链表
if (e.next == null)
//进行哈希和新数组长度与运算,获得当前元素存放在新数组
//的位置
newTab[e.hash & (newCap - 1)] = e;
//如果next是红黑树 (当节点长度大于等于8,链表会转成红黑树)
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//剩下的是链表情况
else {
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) {
//首次插入tail为空 尾,头都为e
if (loTail == null)
loHead = e;
//非首次插入,原最后一个元素next指向当前元素
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;
}
}
}
}
}
return newTab;
}
代码块解析:
Node<K,V>[] oldTab = table;
//旧数组容量 如16
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧数组阈值 如12
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//旧数组容量已大于最大容量,只能提高阈值,其他不变
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果新数组容量(旧数组容量的两倍)小于规定最大容量
//并且旧数组容量大于16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//新数组阈值也为旧阈值两倍
newThr = oldThr << 1;
}
//旧数组阈值大于0
else if (oldThr > 0)
newCap = oldThr;
//旧数组容量和阈值都没定义,设置为默认的16和12
else {
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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
为什么扩容后,元素新下标为原数组下标或者原数组下标+原数组容量?
此行代码解析:
(e.hash & oldCap) == 0
用于判断扩容之后,当前元素是否需要更换索引位置
例子:原来容量为16,扩容到32,元素hash后等于26
已知索引下标位置 index = e.hash & length-1
old:
26: 0001 1010
15: 0000 1111
&: 0000 1010
new:
26: 0001 1010
31: 0001 1111
&: 0001 1010
可以发现,扩容后的唯一区别就是高一位从0变成1低位不变,15有4位,
高一位是第五位
又因为是与运算,所以原本第五位为0,无论hash值为多少,与运算都是0,
如果扩容后与运算还是0,就表示扩容后索引位置不变,什么情况会是0呢,
已经扩容后第五位是1了,那就只能元素hash的第五位是0,与运算后才是0,
和扩容前的索引位置一样。
情况已经知道了
现在的问题:如何确定hash值高一位的位值是0还是1?
其实将hash与oldCap与运算就能知道是0还是1(oldCap为旧数组的长度)
示例1:
e.hash=10 二进制: 0000 1010
oldCap=16 二进制: 0001 0000// & =0 0000 0000
扩容后索引不变,因为现在高一位是1,只有hash的高一位是1,
进行与预算
结果才会是1,是0与运算后则 e.hash & oldCap=0
示例2:
e.hash=17 二进制:0001 0001
oldCap=16 二进制: 0001 0000
& =1 0001 0000 比较高位的第一位 1
这个时候与运算后结果不为1,说明扩容后索引位置有变,
新的下标位置是原下标位置+原数组长度
原因:索引下标位置 index = e.hash & length-1
hash值为26情况,扩容后索引的差别就是高一位由0变成1,其他不变,
这多出来的1就是原数组长度,原本数组长度16 二进制 1 0000
old:
26: 0001 1010
15: 0000 1111
&: 0000 1010
new:
26: 0001 1010
31: 0001 1111
&: 0001 1010
代码块解析: 4个参数
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
//lo,hi为两条链表
//lo 为low的缩写 表示低位
//hi 为high的缩写 表示高位
//heah指向头结点,tail指向尾结点
//因为上处解析,当元素进行扩容时,可以分成两种类型
//1.扩容后索引跟扩容前索引相同 放在lo链表里
//2.扩容后索引为扩容前索引位置+扩容前哈希表长度 放在hi链表里
代码块解析:
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
此时,当前节点链表所有元素已经遍历完成,分别存在lo,hi两个链表里
由于在哈希表同一个节点链表中,可以确定这些元素的低位hash值肯定相同,
高一位中,lo链表为0,hi链表为1
注意:原链表头结点在哈希表数组中
第三行:将新数组第j位索引指向lo链表头节点,j为该链表在旧数组中的索引位置
第七行:将新数组第(旧数组索引+旧数组长度)位索引指向hi链表投节点
备注:
为什么新数组第j或者j+oldCap索引直接指向该链表,不怕旧数组其他位置也放这吗?
索引下标位置 index = e.hash & length-1
新数组这两个位置的低位(最高位外的其余位)索引是lo,hi的哈希值,
又因为hash值相同的元素,一定放在同一个索引下,所以其他位置索引hash值
肯定与lo,hi不同了。