HashMap的长度、Hash算法、HashMap的增删改查、HashMap与concurrentHashMap

HashMap

Hash算法

将任意长度的输入通过Hash算法映射成一个固定长度的输出

问题:

两个Value值经过hash算法之后可能会算出同样的hash值,就会发生hash冲突

理论上无法避免,10个苹果放到9个抽屉,只能尽量避免

比较好的hash算法应该考虑的点:

  • 效率应该高,长文本也能高效输出hash值
  • hash值不应该能推出原文
  • 两次输入有一点不同,要保证hash值不同
  • 尽可能的要分散,table中slot大部分都处于空闲状态时,尽可能降低hash冲突

Hashmap的存储结构

JDK8:数组+链表+红黑树

每个数据单元都是一个Node结构,Node结构中有key字段、value字段、next字段、hash字段

next字段是在发生hash冲突的时候,当前桶位中的node与冲突node连成一个链表要用的字段。

红黑树:一种接近平衡二叉搜索树,在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black,通过对任何一条从根到叶子的路径上各个结点色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。

支持查找、插入,删除等操作,其平时间复杂度最坏为O(logn<n)

查询时间 :数组<红黑树<链表

插入删除 :链表<红黑树<数组

红黑树的5个性质:

每个结点要么是红的要么是黑的。

根结点是黑的。

每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。

如果一个结点是红的,那么它的两个儿子都是黑的。

对于任意结点而言,其到叶结点树尾端NI儿指针的每条路径都包含相同数目的黑结点

默认长度与负载因子

创建Hashmap的时候没有指定散列表数组长度,初始长度是16,散列表是懒加载机制,是在第一次put数据的时候才去创建。

默认的负载因子是0.75,负载因子它的作用就是计算扩容阈值用的,比如说使用无参构造方法创建的hashmap对象,它默认情况下扩容阈值就是也就是16 * 0.75也就是12

链表转到红黑树的条件

链表转红黑树主要是有两个指标其中一个就是链表长度达到8,还有个指标就是当前散列表数组长度已经达到64,否则的话,就算slot内部链表长度到了8,它也不会链转树,仅仅会发生一次resize,散列表扩容,而当长度降到 6 就转换回去

Node对象内Hash字段的hash值

Node对象它内部有一个hash字段,这个hash值是key.hashcode二次加工得到的,加工原则是:key#hashcode高16位^低16位得到的一个新值,

因为hash寻址算法的缘故,散列表数组的长度必须是2的次方数。

比如说就是16、32、64,寻址算法是:hash & (table.length- 1),length转化为二进制后,一定是(1)是高位,然后低位全部是(0),就比如说 16转化为二进制就是1加四个0 ( ps : 10000 )然后,32转化为二进制就是1加五个0 ( ps :100000 ),这种数值它减1之后得到的数值转换为二进制它也很特殊,就比如说:16-1 = 15,然后15转化为二进制就是四个1 (ps : 1111 ),任何(数)与这种高位全部为0,低位都是一串1的数,按位与之后,它得到的这个数值一定是>=0且<=这个二进制数的( ps :假设是15=b 1111 ),然后带上数组的下标为0的slot,加起来正好为2的次方数。

用高低位寻址法的原因

采用高低位异或主要是为了优化hash算法,hashmap内部散列表,它大多数场景下, 它不会特别大,也就说[table.length- 1]得到的这个二进制数, 实际有效位很有限,-般都在(低)16位以内,这样的话,key#hash值高16位就等于完全浪费了,没起到作用。

hashmap的put数据流程

分四种情况:

前面寻址算法是一样的,都是根据key#hashcode经过高低位异或之后的值,然后再按位与& (table.length- 1),得到一个槽位下标,然后根据这个槽内的状况,状况不同,情况也不同

一、slot == null

直接占用slot,把当前put方法传进来的key和value包装成一个node对象放到这个slot中

二、slot != null 并且它引用的node还没有链化

需要先对比一下这个node#key与当前put对象的key是否完全相等?如果完全相等的话, 这个操作就是replace操作,把新的value替换当前slot->node#value ,否则就是一个hash冲突,就得slot->node后面追加一个node,采用尾插法

三、slot内的node已经链化

(会判断链表长度是否达到树化标准)

这种情况和第二种情况处理很相似,首先也是迭代查找node,看链表上的元素的key,与当前传来的key是不是完全一致,如果有一致的话,还是repleace操作,替 换当前node#value,否则的话,迭代到链表尾节点也没有匹配到完全一致的node,这就有hash冲突,把put(数据)包装成node追加到链表尾部,还需要再检查一下当前链表长度有没有达到树化阈值,如果达到阈值的话就调用一个树化方法,树化操作都在这个方法里完成

四、冲突很严重的情况下,链表已经转化成红黑树

按照红黑树的方式进行插入或覆盖。

put方法源码

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
    	//判断表是否为空,如果为空,则调用resize()函数初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
    	//如果没有hash冲突,直接插入即可
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        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);
                        //如果链表长度到达了转换陈红黑树的长度,执行treeifyBin()方法
                        //如果hashMap数组长度小于64,则进行扩容操作
                        //否则进行树化操作
                        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;
                }
            }
            //如果存在相同的key,则考虑值覆盖
            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;
    }

Hashmap的扩容机制

什么条件下会触发扩容?

写数据可能就会触发,hashmap结构内有记录当前数据量的字段,数据量的字段达到扩容域的时候会触发扩容操作。

扩容规则:

table的数组长度必须是2的次方数,每次按照上一次的tablesize位移运算得到,做左移一位运算,假设当前tablesize是16,左移一位就是32,采用位移运算是因为cpu的所有乘法运算都是在指令层通过加法计算得到,效率相对较低,用位移运算简洁高效

最大容量限制:

int类型的数据所占空间大小为32位,所以如果超过这个范围之后,会出现溢出。所以,1<<30是在int类型取值范围中2次幂的最大值,即为HashMap的容量最大值。

老数组的数据迁移问题:

挨个桶位推进迁移,就是一个桶位一个桶位的处理,主要还是看当前处理桶位的数据状态,四种状态

第一种:slot存储的是null,不用处理;

第二种:slot存储的是个node,但node没有链化:

当迁移的时候发现slot中存储的node节点的next是null的时候,说明这个slot它没有发生过hash冲突,直接迁移就好,根据新表的tableSize计算出…它在新表的位置,然后存放过去就ok了

第三种:slot存储了一个链化的node:

迁移发现它这个next字段,这个node#next字段它不是null,说明这个slot发生过hash冲突.这个时候需要把当前slot中保存的这个链表拆成两个链表,分别是这个,高位链和低位链,老表中这个桶位已经链化了,这样就推理出这个链表中所有的node#hash字段转化成二进制以后,低位都是相同的,低位指的就是老表的tablesize-1转化出来的二进制数的有效位,比如table数组长度是16 , 16-1就是15,然后15转化出来的二进制数就是( 1111 ),说明低位是低四位,高位是第5位(ps:01111),则当前这个链表的低位一定是相同的,但高位(第五位)不一定,有的可能是0有的可能是1,这块对应的node迁移到新表中,它所存放的slot位置也是不一样的,低位链因为它的高位是0,所以迁移到node新表的时候,这个slot下标和老的是一样的,但高位链因为高位是1,所以说存储到新表(扩容)了之后,是老表的位置+老表的size,比如说老表的table长度是16,数据存放在老表(下标)8的位置,那存储扩容后表的位置是8+16,也就是24

如果n & hash == 0,则 (2n-1) & hash == (n-1) & hash ;

如果n & hash != 0,即 (2n-1) & hash == (n-1) & hash + n ;

所以可以根据n & hash的结果,将链表中的元素分成两个链表,低位依旧放在原位置,高位放在原位置+n处。

第四种就是存储了一个红黑树的根节点TreeNode对象;

红黑树节点对象, 这个TreeNode结构,它依然保留了这个next字段,也就是说红黑树这个结构,它内部其实它还维护着一个链表,不过查询的时候不用它,但是新增或者删除的时候依旧会维护链表,这个链表方便split 拆分红黑树,处理和普通node链表没区别,也是根据高低位拆分,高位存储到新表的位置依旧是老表的位置+老表的数组长度,低位链存放的位置和老表相同,不同点在于拆分出来的高低位链表需要看长度,如果<=6直接将TreeNode转化为普通的node链表放到新表中,若依旧大于6,依旧会扩展为TreeNode

//源码解释
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; // 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;
    	//以下是rehash的过程
        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;
                    //如果是红黑树结构,则进行分裂操作
                    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;
                            //第一种情况:n&hash == 0
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            //第二种情况:n&hash != 0
                            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;
    }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值