美文网首页
HashMap解析(2)

HashMap解析(2)

作者: 迷糊小生 | 来源:发表于2019-04-05 14:05 被阅读0次

    上一篇对HashMap的结构做了详细的介绍,讲解了put方法还有get方法,本篇将会更深入的走进HashMap源码。

    散列函数(解释hash碰撞)

       public V get(Object key) {
            Node<K,V> e;
            return (e = getNode(hash(key), key)) == null ? null : e.value;
        }
    
        public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    

    在上篇文章我们讲解的get和put方法中都使用了hash(key),现在让我们一起来看下这个hash函数究竟是什么吧。

       /**
         * 将高位与低位进行与运算来计算哈希值。因为在hashmap中使用2的整数幂来作为掩码,所以只在当前掩码之上的位上发生
         * 变化的散列总是会发生冲突。(在已知的例子中,Float键的集合在小表中保持连续的整数)因此,我们应用一个位运算
         * 来向下转移高位的影响。 这是在综合考虑了运算速度,效用和质量之后的权衡。因为许多常见的散列集合已经合理分布
         * (所以不能从扩散中受益),并且因为我们使用树来处理bin中发生的大量碰撞的情况,所以我们尽可能以代价最低的方式
         * 对一些位移进行异或运算以减少系统损失, 以及合并由于hashmap容量边界而不会被用于散列运算的最高位的影响。
         */
          //将key的hashCode的高16位与低16位做了一个异或操作
        static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    
    image.png

    n是table的大小,默认是16,二进制即为10000,n - 1 对应的二进制则为1111,这样再与hash值做“与”操作时,就变成了掩码,除了最后四位全部被置为0,而最后四位的范围肯定会落在(0~n-1)之间,正好是数组的大小范围,散列函数的妙处就在于此了。

    image.png

    就拿昨日我们的例子做实验进行计算:

    “噢噢”的hash值为707658,转化为二进制就是:10101100110001001010,table大小是32,n-1也就是31对应的二进制则是:11111


    image.png

    运算之后的结果是:01010,也就是10,所以“噢噢”的下标就落在了table的10号位

    再来看“啊啊”,它的hash值为698698,对应的二进制是10101010100101001010,table大小是32,n-1也就是31对应的二进制则是:11111


    image.png

    运算之后的结果是:01010,也同样是10号位,因此发生了hash碰撞,形成了链表。

    扩容函数

    在之前我们看过了HashMap中put方法的源码,我们可以经常看到resize()这个方法,这个方法是用来对hashmap进行扩容的,现在让我们一块来对其分析一下

    /**
       * 初始化或将table的大小进行扩容。 如果table为null,则按照字段threshold中的初始容量目标进行分配。
       * 否则,因为我们使用2次幂进行扩容,所以在新表中,来自每个bin中的元素必须保持在相同的索引处,或者以原偏移量的2次幂进行移动。
       */
    final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;
            //扩容前table容量
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            //扩容前阈值
            int oldThr = threshold;
            int newCap, newThr = 0;
            //如果旧容器的容量不为0
            if (oldCap > 0) {
                //如果扩容前的容量大于等于HashMap的最大容量,则将其阈值设置为2的31次方减1(MAXIMUM_CAPACITY的值为2的30次方,永远达不到阈值,不再对其进行扩容),并返回原容器
                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) //旧容器容器容量为0,但是阈值不为0时,初始容量被设置为阈值
                newCap = oldThr;
            else {               // 旧容器容器容量为0,阈值为0时,使用默认出初始化容器大小与阈值
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            if (newThr == 0) {
            //旧容器容器容量为0,但是阈值不为0时,此时新容器大小被赋予上了阈值的大小
                //给新容器设置新阈值
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
            threshold = newThr;
            //将旧数组中的node重新散列到新数组中
            @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) {
                        //节点不为null
                        oldTab[j] = null;
                        if (e.next == null)
                            //如果该节点为没有子节点,则直接放入到新table中
                            newTab[e.hash & (newCap - 1)] = e;
                        else if (e instanceof TreeNode)
                            //如果该节点为树节点,则对树的hash重新分配
                            ((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;
                                //如果该节点为新容器的0号位
                                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使用的是懒加载的方式,在构造函数中并没有初始化table,而是再添加第一个数据的时候在扩容方法中初始化了table。

    当使用put插入元素的时候,如果发现目前的bins占用程度已经超过了加载因子所设置的比例,那么就会发生resize,简单来说就是把原来的容量和阈值都调整为原来的2倍,之后重新计算数组索引,把节点再放到新的bin中。因为index值的计算与table数组的大小有关,所以扩容后,元素的位置有可能会调整。

    拿之前的“订单”为例,在没有扩容之前:


    image.png
    image.png

    经过计算在容量为8的时候的确在2号位上

    但是,在扩容到size为64的时候我们再计算一下它的位置


    image.png

    经过计算它应该在34号位置上,而事实也正是如此,扩容后元素的位置发生了改变。

    image.png

    相关文章

      网友评论

          本文标题:HashMap解析(2)

          本文链接:https://www.haomeiwen.com/subject/igpciqtx.html