HashMap添加、转换为红黑树、扩容源码分析

HashMap添加、转换为红黑树、扩容源码分析

put方法

  1. 通过hashcode计算出key映射到哪个桶

  2. 如果没有发生hash冲突直接插入

  3. 如果发生了hash冲突久处理冲突

    1. 如果使用的是红黑树处理冲突,那么就调用红黑树的方法插入数据
      2. 如果是链表方法插入,那么就按照链表方式插入,如果达到了临界值就把链表转换为红黑树

4.如果桶中存在重复的键,那么就替换value

5.如果size大于预值,那么就扩容

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

计算hash的方法

static final int hash(Object key) {
    int h;
    //先hashcode与hashcode无符号右移16位异或得到最后的hash值
    //如果key为null,那么就默认hash值为0
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

h = key.hashCode() ^

(h >>> 16);

hash值得计算方法为

1.哈希无符号右移16位与hashcode的亦或

1111 1111 1111 1111 1111 0000 1110 1010 h

0000 0000 0000 0000 1111 1111 1111 1111 h >>> 16


1111 1111 1111 1111 0000 1111 0001 0101 返回的hash

p = tab[i = (n - 1) & hash] 返回的hash与数组长度-1进行位或与得出指定索引下标

n为数组长度,因为hash%数组长度 等价于 hash % (n-1) 因为%效率低所以用n-1

1111 1111 1111 1111 0000 1111 0001 0101 返回的hash

0000 0000 0000 0000 0000 0000 0000 1111 (假设现在长度为16) 16-1=15


0000 0000 0000 0000 0000 0000 0000 0101 为5

这么做是为了减少hash冲突,如果不这么做直接拿hashcode ^ hashcode >>> 16 当hash值高位变化很大地位变化很小,这样就容易造成hash冲突

putVal()

/**
 * hash 计算后的hash值,
 * key  key
 * value v
 * onlyIfAbsent 如果为true不更新现有值
 * evict如果为false,表示table为创建状态
 **/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //n是数组长度
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果我们表为null或者表的长度为0那么我们就扩容进行
    //resize方法的解析在下面
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //i = (n - 1) & hash 等价于  hashcode % 数组长度 
    //因为取余效率低所以用的数组长度-1 &(按位与) hash 算出来索引下标
    //判断当前下标是否为null,如果为null直接插入进入
    if ((p = tab[i = (n - 1) & hash]) == null)
        //按照链表的处理方式去插入
        tab[i] = newNode(hash, key, value, null);
    //发生当前下标有数据
    else {
        Node<K,V> e; K k;
        //比较hash值是否相同
        if (p.hash == hash &&
            //查看地址是否相等			查看k是否相等
            ((k = p.key) == key || (key != null && key.equals(k))))
            //如果相等就替换value
            e = p;
        //判断是否为红黑树
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //遍历链表
            for (int binCount = 0; ; ++binCount) {
                //如果当前链表节点next为null就直接插入进去
                if ((e = p.next) == null) {
                    //让链表先插入数据在判断是否满足红黑树
                    p.next = newNode(hash, key, value, null);
                    // 如果当前链表长度大于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 = e;
            }
        }
        //如果e不为null久不执行替换操作了
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            //查看是否满足修改需求
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;	//记录map修改次数
    //查看是否大于阈值,如果大于了就扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

通过这个代码我们可以知道在jdk1.8 hashmap是采用的尾插法,是让链表先插入数据在判断是否满足红黑树

resize 第一次使用的情况下

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) {
 		......
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
       .........
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;								//将默认容量赋值给newCap
        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) {
     	............
    }
    return newTab;
}

通过这个代码我们可以得知,在jdk1.7以后,只要在hashmap集合第一次使用的时候才会创建数组

转换红黑树的过程

/**
 * Replaces all linked nodes in bin at index for given hash unless
 * table is too small, in which case resizes instead.
 * 替换给定哈希索引处 bin 中的所有链接节点,除非表太小,在这种情况下调整大小。
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //如果数组等于null 或者 长度没到64()就扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //创建红黑树的子头节点和尾节点
        TreeNode<K,V> hd = null, tl = null;
        do {
            //创建一个树节点 内容和当前链表节点保持一致
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                //将新创建的p节点复制给红黑树的头节点
                hd = p;
            else {
                //将上一个节点p赋值给现在的p的前一个节点
                p.prev = tl;
                //将现在节点p作为树的尾节点的下一个节点
                tl.next = p;
            }
            tl = p;
            /*
             * 遍历遍历
             */
        } while ((e = e.next) != null);
        //让桶中第一个元素即数组中的元素指向新创建的红黑树,以后这个桶里就是红黑树而不是链表了
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

通过上面代码我们得知**,如果数组为null,或者数组长度不大于64不会转为为红黑树的,只会扩容**

​ 如果是转换红黑树就遍历桶中元素,然后创建相同个数的树形节点,复制内容然后建立连接

​ 然后让桶中的第一个元素指向新创建的树根节点,替换桶中的链表为树形化的内容

扩容

什么时候扩容?

​ 当HashMap中的元素个数超过数组长度*负载因子的时候,就会扩容,负载因子(loadFactor)的默认值为0.75,

​ 当HashMap中其中一个桶的链表长度达到了8个,此时数组长度没有达到64,那么HashMap就会扩容

注意: 扩容是比较消耗性能的

(数组长度-1) & 计算后的hashcode 的扩容前后的下标区别

(数组长度-1) & 计算后的hashcode
    /*
     *	HashMap未扩容之前
     */
 	
    //Example A
	0000 0000 0000 0000 0000 0000 0001 0000     16		//当前数组长度
    
    0000 0000 0000 0000 0000 0000 0000 1111     16 -1 	//数组长度-1
    1111 1111 1111 1111 0000 1111 0000 0101				//计算后的hash值
---------------------------------------------------------
    0000 0000 0000 0000 0000 0000 0000 0101				//(数组长度-1) & 计算后的hashcode  索引为5

    //Example B
    0000 0000 0000 0000 0000 0000 0000 1111     16 -1 	//数组长度-1
    1111 1111 1111 1111 0000 1111 0001 0101				//计算后的hash值
---------------------------------------------------------
    0000 0000 0000 0000 0000 0000 0000 0101				//(数组长度-1) & 计算后的hashcode  索引为5
    
    /*
     *	HashMap扩容之后
     */
    
    //Example A
    0000 0000 0000 0000 0000 0000 0010 0000     32		//当前数组长度
    
    0000 0000 0000 0000 0000 0000 0001 1111     32 -1 	//数组长度-1
    1111 1111 1111 1111 0000 1111 0000 0101				//计算后的hash值
----------------------------------------------------------
    0000 0000 0000 0000 0000 0000 0000 0101				//(数组长度-1) & 计算后的hashcode  索引为5
    
    //Example B
    0000 0000 0000 0000 0000 0000 0001 1111     32 -1 	//数组长度-1
    1111 1111 1111 1111 0000 1111 0001 0101				//计算后的hash值
----------------------------------------------------------
    0000 0000 0000 0000 0000 0000 0001 0101				//(数组长度-1) & 计算后的hashcode  索引为5 + 16

HashMap的扩容是扩容原来的2倍

e.hash & oldCap

如果e.hash & oldCap

	0000 0000 0000 0000 0000 0000 0001 0000     16		//oldCap
    1111 1111 1111 1111 0000 1111 0000 1101				//e.hash
-----------------------------------------------------
	0000 0000 0000 0000 0000 0000 0000 0000     0		// e.hash & oldCap*

拿当前例子来说当oldcap为16的时候 16的二进制为0001 0000,因为hashcode是随机的,所以右数第5位可能是0也能是1。

在JDK1.8后扩容HashMap的时候,就不需要重新计算Hash值,

​ 如果当前桶只有一个节点的话那么直接通过e.hash & (newCap - 1) 当前节点存放的hash & (新的数组容量-1)

​ 如果当前桶有多个链表节点就拿oldcap为16的时候来说,他的二进制为10000,然后hashcode是随机的,所以他的二进制也是随机的,然后就看它的右数第5位来说,如果右数第五位是1那么就到新的数组下标为当前旧数组的下标+oldcap,如果是0那么它在旧的数组下标就是它到新的数组所在的下标。

​ 如果是红黑树: 就把红黑树拆分成了链表放入出在新的数组

源码

/*
 *  不是第一次插入的情况
 */

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主要确认了新的容量和新的边界值
     */
    if (oldCap > 0) {
        //查看旧数组是否大于数组最大的容量
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //oldCap << 1 等价于 oldCap *2
        //从这里我们得出 HashMap的数组扩容是扩容2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //边界值是旧的边界值*2  oldThr << 1等价于 newThr = oldThr *2
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        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)
                    //e.hash & (newCap - 1) 就是我们上面提到的,
                    newTab[e.hash & (newCap - 1)] = e;
                //如果当前桶是红黑树的就按照红黑树的处理方案
                else if (e instanceof TreeNode)
                    //把红黑树拆分了
                    ((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;
                        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;
                    }
                }
            }
        }
    }
    return newTab;
}

总结

HashMap的添加流程为

​ 1.通过hash值计算出key映射到哪个桶里面

​ 2.如果没有发生hash冲突就直接放进去

​ 3.如果发生了hash冲突就判断是使用链表解决冲突还是用红黑树,如果是用链表就用链表的方式插入,如果是红黑树就按照红黑树插入,如果链表长度大于等于临界值,数组长度大于等于64就将链表转换为红黑树

​ 4.如果桶中存在该key值,就替换value

​ 5.判断size是否大于预制,如果大于了就扩容

索引下标的计算方法

首先通过hashcode 与 hashcode无符号右移16位进行亦或得出来的新的hashcode ,然后的出来的结果在与数组长度-1进行按位与的出来指定索引下标。(hash ^ (hash>>>16) & length-1)

这么做的原因就是减少hash碰撞,把数据均匀分配,让每个链表长度大致 相同。还有提高效率,因为如果用取余去做的话,在计算机效率是很低的

Hashmap创建数组的机制

在jdk1.7以后,只要在hashmap集合第一次使用的时候才会创建数组

HashMap采用的插入方式

​ HashMap使用的尾插法在jdk1.8

转换红黑树的过程

​ 当数组为null,或者长度不大于64只会去扩容而不会转换为红黑树,只有链表长度大于8并且数组长度大于64就会转为红黑树

​ 如果转换红黑树,然后创建相同个数的树形节点,赋值内容然后建立连接

​ 让后让桶中的第一个节点指向树的根节点,然后将桶中的链表替换为红黑树

为什么大于8而不是大于等于8疑问解决

​ 因为在HashMap是先让链表插入数据后在去判断是否满足转换为红黑树,所以是大于8

扩容

HashMap1.8是在第一次put的时候会调用扩容方法进行创建数组

​ 当集合中元素的数量大于 边界值(数组总容量*负载因子)就会进行扩容,或者当链表长度大于8并且数组长度小于64会扩容。每次扩容是2倍。

​ 在hashmap1.8对扩容做了一些优化

​ 如果当前桶链表只有一个节点那么就直接通过hash & (新的数组长度-1)

​ 如果当前桶链表有多个节点,当hash & oldcap 为 0 那么就插入新的数组的下标和旧的数组下标一致,如果为1那么就插入新的数组下标为旧的数组下标+旧的数组长度

​ 如果是红黑树那么就把红黑树拆分为链表放入新的数组

jdk1.8 hashmap是采用的什么插入方法

jdk1.8 hashmap是采用的尾插法

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哇塞大嘴好帅(DaZuiZui)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值