深入了解HashMap(看完你就懂了)

1.HashCode

        HashCode为什么用31作乘数?

        HashCode乘数为31的原因:

                由实验证明乘数越大hash碰撞的几率就越小,当乘数到达31后碰撞几率已经非常小,后面增大乘数碰撞几率下降不明显,又应31是最小的奇质数相乘不容易造成数据溢出,所以选择31为乘数。

2.扰动函数

        在使用HashMap我们经常使用put方法,那HashMap如何定位存储的位置呢?我们知道hash碰撞越多,越影响性能,那HashMap是如何优化的呢?

 查看源码发现这里有一个计算hash的方法,也就是获取存储位置的方法,那我们具体研究一下   

 hash()

         从源码中不难发现当我们put的key为空是默认存储的是第一个位置,也证明了HashMap key能为空,后面这段 (h = key.hashCode()) ^ (h >>> 16)什么意思呢?前面是获取key的hash值,然后异或上hash值右移16位。为什么要这样异或hash值来获取位置呢?其实这就是HashMap的扰动函数,当hash值异或hash值右移16,就是hash的高低位异或,增加了随机性,减少hash碰撞。

 3.初始化容量和负载因子和扩容

        在使用HashMap我们经常使用put方法,可是你知道HashMap new出来的时候初始容量是多少?是如何扩容的?负载因子是什么?

 put方法调用putVal方法,研究一下putVal()

 既然知道扩容方法为resize()那研究一下

 上图没有实际数据,看看下图证明一下,初始化阀值16,负载因子0.75

        从源码中不难看出,hashMap的初始容量是16,负载因子的0.75用来作乘数,初始容量*负载因子等于阀值12的时候进行扩容也就是当到达容量的3/4进行扩容。那当数据达到12的时候要进行扩容又是怎么样的呢?细研究下resize()

1.阀值和容量的计算

         总结一下,这里主要是阀值和容量的计算,判断阀值是否已经到达最大容量2的30次方,达到了直接让他等于Integer的最大值2的31次方-1,判断阀值是否*2小于最大容量且大于16,则阀值为旧阀值*2,如果初始化设置了容量则,新容量等于阀值,新阀值等于旧阀值*负载因子。有点绕,细品!(补充:图中的最大容量和integer的最大数写错了,分别是2的30次方和2的31次方-1)。知道了扩容,那扩容之后的数据需要重新排序存放,是怎么重排存放的呢?

2.扩容后的数据重排序

         总结一下,这里分为3种情况一、数据没有形成链表和树,此时重新hash值计算下标然后插入新的扩容node对象即可。二、数据没有形成链表但是形成树,这里需要进行树的重排序,感兴趣的可以研究一下。三、数据形成链表但是没有形成树,这里分为扩容后需要改变数据下标的数据(原索引位置+原容量)和扩容后不需要改变下标的数据(原索引位置),分别形成链表,然后把头结点指向对应的下标。最后返回扩容后重排序的数据。

已经了解了扩容的基本方式,那当初始化的时候进行,定义初始容量HashMap又是怎么样的呢?

 研究一下带参构造器

 看下this

 看下阀值的计算方法tableSizeFor()

 从中可以看到当我们初始化容量为9的时候,阀值threshold为16,当初始化提供了容量参数容量等于阀值16,上面的扩容方法resize()有说到,也不难看出当初始化提供了容量参数默认会找到离初始化容量参数最近的2的n次方,9的话就是16,17的话就是32 ....,好了扩容、负载因子、初始化容量研究完毕!

4.put()

        经常使用put方法,但你知道put方法具体是实现的吗?会用就行?更深入了解才行!好了研究完了扩容接下来深入研究一下put(),上面已经说过看put调用的是putVal()

 /**
     *
     * @param hash 计算的hash值
     * @param key put传入的key
     * @param value put传入的值
     * @param onlyIfAbsent 如果为true即使key已存在,也不会替换value
     * @param evict 如果为false,数组table在创建模式中
     * @return 如果value被替换,返回旧value,否则返回null
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //tab是内部数组、p是数组中的首节点、n是数租长度、i是索引下标
        HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
        //初始化的时候tab为空需要扩容,初始化进入此判断,赋值n等于扩容后的长度
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //当此节点没有数据也就是等于空的时候进入此判断,赋值节点的值tab[i],赋值p
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //到这个判断表示这个节点有数据,需要添加形成链表或树或者覆盖原来的值
        else {
            //创建node和key用于存储数据
            HashMap.Node<K,V> e; K k;
            //如果key相同,覆盖原来的对象e=p,其实就是覆盖原来的值
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果已经形成树,进行树节点的处理
            else if (p instanceof HashMap.TreeNode)
                e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //走到这表示要将数据添加到链表里
            else {
                //循环链表,并记录循环数量
                for (int binCount = 0; ; ++binCount) {
                    //如果p.next等于空表示循环到了链表尾节点
                    if ((e = p.next) == null) {
                        //连接上链表的最后一个节点
                        p.next = newNode(hash, key, value, null);
                        //进行判断binCount循环数量是否大于等于7,因为开始的时候++过一次使用大于等于7,链表长度是否大于等于8
                        //如果是则尝试把链表转化成红黑树,注意是“尝试”,不一定是转化成红黑树也有可能是扩容
                        //只有当数据桶长度大于64,链表长度大于等于8的时候才转化成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //不断循环链表当在链表的某一个节点遇到相同的key直接break;此时上面的e就是找到的节点
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //每次让p=e,方便遍历下一个节点
                    p = e;
                }
            }
            //这里就是处理上面得到的e
            if (e != null) { // existing mapping for key
                //替换原来的值
                V oldValue = e.value;
                //如果onlyIfAbsent为false或者oldValue为空证明需要替换原来的值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //空方法,用于LinkedList,无关
                afterNodeAccess(e);
                //返回旧值
                return oldValue;
            }
        }
        //统计在hashmap上被修改次数,无关
        ++modCount;
        //插入了一个数据++然后比较是否大于阀值,大于则扩容
        if (++size > threshold)
            resize();
        //空方法,用于LinkedList,无关
        afterNodeInsertion(evict);
        //key等于null,直接返回null
        return null;
    }

/**
     * @param tab HashMap数组桶数据
     * @param hash key的hash值
     */
    final void treeifyBin(HashMap.Node<K,V>[] tab, int hash) {
        //n记录tab的长度、index记录索引下标、e记录节点
        int n, index; HashMap.Node<K,V> e;
        //如果tab等于空或者数组桶长度小于64则进行扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        //到这里表示链表长度大于8,数组桶长度大于64,转化成树
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            //hd存储树链表的头节点、tl存储树链表的尾节点
            HashMap.TreeNode<K,V> hd = null, tl = null;
            //一直循环e=e.next直到为空也就是循环到数组桶尾部了
            do {
                //每次用树节点装一个数据
                HashMap.TreeNode<K,V> p = replacementTreeNode(e, null);
                //这里就是一开始进入,把头节点记录下来,然后循环就再也不进入了
                if (tl == null)
                    hd = p;
                //这边是首次不进入,然后循环一直进入,把尾节点一直链接形成树链表,细品
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                //每次都把尾节点赋值为p,方便形成链表
                tl = p;
            } while ((e = e.next) != null);
            //把对应下标的tab赋值为红黑树链表,然后形成红黑树
            if ((tab[index] = hd) != null)
                //把红黑树链表转化成红黑水
                hd.treeify(tab);
        }
    }

总结一下,这里其实就是一些扩容和链接方式的处理,主要有,判断是否是初始化,是初始化就进行扩容,判断key是否一致,一致覆盖值,判断是否树化,树化了用树的处理方式,判断是否形成链表:1.是否需要转化成树(数组桶长度大于64和链表长度大于等于8)、2.直接添加、3.节点有key相同替换掉值。最后进行节点赋值,判断是否需要扩容。源码有点多如果细细去看每一行细品,会发现看懂其实不难主要还是要学习其中的思想和一些写代码的方式。

3.get()

        了解了put(),那知道HashMap获取数据又是怎么样的呢?有点好奇,研究一下get()

   /**
     * 
     * @param key key
     * @return key对应的值
     */
    public V get(Object key) {
        //定义node用于存储数据
        HashMap.Node<K,V> e;
        //如果node为空直接返回null,否则找到node.value返回对应的值,调用了getNode(),研究一下
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    /**
     *
     * @param hash key的hash值
     * @param key key
     * @return 当有值的时候返回值,没有返回空
     */
    final HashMap.Node<K,V> getNode(int hash, Object key) {
        //tab存储node数据、first首节点、e循环节点、n数据长度、k存储key的值
        HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
        //存储的hashmap数据不能为空,长度必须大于0,key的首节点不能为空,其中一个不满足直接返回null
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
            //如果找到key刚好是首节点返回首节点
            if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //到这表示key对应的数据不在首节点,需要从树或者链表中找出数据
            if ((e = first.next) != null) {
                //如果已经形成树则用树的方式找点节点并返回
                if (first instanceof HashMap.TreeNode)
                    return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
                //到这表示是链表,不断循环取出链表的节点,直到取完
                do {
                    //如果找到hash相同且key相等的node直接返回
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        //没有找到node返回null
        return null;
    }

总结一下,这里就比前面的简单多了,方式还是那几个,首先获取到数据node,获取node的过程中需要HashMap有数据,且有对应key,否则返回空。如果有数据先找数组桶里有没有对应下标key相同的数据有返回node,找不到循环节点从链表或者红黑树里找到node返回,最后通过node.value得到对应的值。

4.remove()

        接下来看下remove()吧,和之前大同小异,干就完了!

  /**
     * key相同移除
     * @param key key
     * @return 如果为空返回空,否则返回移除的值
     */
    public V remove(Object key) {
        HashMap.Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
                null : e.value;
    }

     /**
     * 要key和value都相同才移除
     * @param key key
     * @param value value
     * @return 如果为空返回空,否则返回移除的值
     */
    @Override
    public boolean remove(Object key, Object value) {
        return removeNode(hash(key), key, value, true, true) != null;
    }

    /**
     *
     * @param hash key的hash值
     * @param key key
     * @param value 值
     * @param matchValue 如果这个为true则要值value相同才移除
     * @param movable 如果为false,则在删除时不移动其他节点
     * @return 返回节点,如果没有则为空
     */
    final HashMap.Node<K,V> removeNode(int hash, Object key, Object value,
                                       boolean matchValue, boolean movable) {
        //tab用于存储数据、p存储节点、n数组桶长度、index索引下标
        HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, index;
        //数据不为空且长度大于0且对应索引数据不为空,则进行移除,否则返回空
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (p = tab[index = (n - 1) & hash]) != null) {
            //定义node存储节点后面用于删除、e存储循环时链表或树的节点、k存储key、v存储值
            HashMap.Node<K,V> node = null, e; K k; V v;
            //如果找到key则把node赋值对应的节点,后面移除
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            //如果p.next不为空,表示已经形成链表或树,并且数据不在第一个节点
            else if ((e = p.next) != null) {
                //如果是树则进行树的处理获取节点,后面移除
                if (p instanceof HashMap.TreeNode)
                    node = ((HashMap.TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    //到这表示是链表,不断循环取出链表节点比较key,如果一样node赋值对应的节点,后面移除
                    do {
                        if (e.hash == hash &&
                                ((k = e.key) == key ||
                                        (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //node不为空
            //如果matchValue为true则需要value值相等才进行下面的移除操作
            //matchValue为false直接进入移除操作
            if (node != null && (!matchValue || (v = node.value) == value ||
                    (value != null && value.equals(v)))) {
                //如果是树通过树的方式移除
                if (node instanceof HashMap.TreeNode)
                    ((HashMap.TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                //如果刚好首节点节点等于得到的node数据节点直接让他指向原来数据的下一个
                else if (node == p)
                    tab[index] = node.next;
                //到这表示节点在链表中找到,p、node现在刚好就是那个节点,直接把p连接到node的下个节点,刚好移除了node节点
                else
                    p.next = node.next;
                //hashmap的修改次数加1
                ++modCount;
                //数据大小减1
                --size;
                //这个方法是空的,无关
                afterNodeRemoval(node);
                //返回被移除的node
                return node;
            }
        }
        return null;
    }

总结一下,这里是先找到对应的node节点和get一样,然后判断matchValue是否为true,为true需要值相等才移除。如果刚好第一个节点就是要找的节点则直接替换掉,如果是树就用树的处理方式,如果在链表中,直接找到节点然后把节点的下一个节点连接上节点的下一个节点。有点绕解释一下:比如 p:node->node1->node2->node3 ,p1:node->node1->node2->node3,我要移除node1是不是:p:node->连接上p1:node2->node3,得到node->node2->node3,细品吧!

最后,就到这吧剩下的代码都比较简单,后面还有红黑树比较难,就不做解释了,细品!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值