hashMap1.8 resize()个人解读

参考文章:
https://blog.csdn.net/u013494765/article/details/77837338

https://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低位不变,154位,
 高一位是第五位
 又因为是与运算,所以原本第五位为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不同了。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值