美文网首页技术干货程序员
HashMap源码浅入深出(二)

HashMap源码浅入深出(二)

作者: 加薪猫 | 来源:发表于2017-09-08 18:02 被阅读0次

    简书 加薪猫
    转载请注明原创出处,谢谢!
    这一系列主要介绍HashMap(1.8),记录也是分享,欢迎讨论

    上回书说到get方法的爱恨情仇,其实主要还是简单介绍了HashMap的一些基本结构,还有HashMap的哈希值是怎么算出来的,这些可以为我们的put方法打下一定的基础,今天就来分享put方法

    2.put(K,V)

    老样子,先怼源码

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

    put方法完结撒花,下面我们来讲putVal.(本猫一系列都将以JDK1.8为主讲述,如果有心人看源码时发现自己的源码可能是个假源码的请更换JDK版本,hash()方法可看上节)

    putVal(int hash,K key,V value, boolean onlyIfAbsent,boolean evict)
    

    先介绍这个方法的几个参数:
    hash就是上节我们说过的,经过HashMap处理过的哈希值
    key和value不谈,onlyIfAbsent这个参数的作用在于,如果我们传入的key已存在我们是否去替换,true:不替换,false:替换。
    参数evict是用在LinkedHashMap的,这里暂且不谈,等之后分享LinkedHashMap的时候我们再来细细分析
    我们先看putVal的代码

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i; //①
            if ((tab = table) == null || (n = tab.length) == 0)//②
                n = (tab = resize()).length;
            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);
                            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;
                    }
                }
                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;
        }
    

    以上代码很长,加上手机上显示好像没有高亮,看着更是难受。所以读者可以以上面为机构,我会在下面拆分进项讲解。下面的拆分和上面的注解一起看,效果更加哦。

     Node<K,V>[] tab; Node<K,V> p; int n, i;
    

    第一次看源码的时候,很容易搞混变量,尤其是看在中间的时候会突然走个神就想着,哎?我是谁,我在哪,我在干什么。。所以最好的方式,还是在看代码时,当你看懂这个变量的意义的时候就加上自己的注释。
    tab:HashMap里面存储Node节点的数组
    p:当前要put进HashMap的key,经过计算得出的tab下标所对应第一个Node。(啥?我在说啥? 第一个Node的意思是,我们的Node本来就是链表中的一个节点,我们根据Key只能找到这个链表的头) 这个更精确地说是当前处理的节点
    n:tab当前的大小
    i:key算出来的下标

            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
    

    table是全局变量,先设置tab= table, n = tab.length,判断如果tab为null,或者tab的大小为0的话,我们就要调用resize()方法进行第一次扩容(resize()方法我们在下面详细说,这里你可以暂时理解为初始化HashMap),从这里我们可以知道的一点是HashMap不是在实例化话的时候就生成指定大小的Node<K,V>[],而是在第一次put时才会进行初始化。

    if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
    

    这里是我们根据计算的哈希值(具体计算方式可以看上一节)算出下标的方式,(n-1)& hash,(15&3=3,15&18=2,与运算这里就不多说了哈~),如果这算出来的下标i,tab[i]为空的话,我们可以直接tab[i]下塞一个新的Node就好了。

    ④ 仿佛又是一大块,这也是这个函数重点理解的地方,⑤~⑪可以就看下面的代码,当然我上面也会标注的
    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);
                            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;
                    }
                }
                if (e != null) { // existing mapping for key ⑪
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
    

    这一段主要讲的当算出的索引下标已经有值时HashMap的处理方式。

    Node<K,V> e; K k;
    

    e:如果要put进HashMap的key已存在,则e为这个key的节点,如果不存在则为null。
    k:遍历链表时,当前节点的key

                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
    

    总是判断第一个,如果算出的hash值相同并且key相同(这个key相同是指要么是同一个对象,要么是equals返回true),那么e=p然后直接进入⑪的判断

                else if (p instanceof TreeNode)
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    

    如果p已经是TreeNode类了(也就是已经转换成红黑树了)就直接调用红黑树的putTreeVal,这个方法之后我们再具体讲,这里只要知道如果是key不存在这个方法返回null,如果存在返回对应的节点进入⑪的判断

    for (int binCount = 0; ; ++binCount) {
    .....
    }
    

    这个循环主要是用来记录链表的数量,之后如果超过默认值(8),会将链表转换为红黑树

    if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
    

    p.next==null的意思是指我们遍历链表已经找到最后一个节点了,那就是虽然算出下标下是有链表的,但是并没有相同key,所以直接创建新的节点放在链表的最后。同时,如果这个链表的长度已经大于默认值8就会将当前这个链表转换为红黑树,具体treeifyBin()方法我们之后分享红黑树的时候来分享。

    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
    

    如果要put的key已经被找到了,则直接进入⑪判断

    if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
    

    一般我们的put的方法都是会直接覆盖旧值的,其实一开始就想说的就是put方法是有返回值的!如果有旧值返回旧值,没有旧值就返回null。afterNodeAccess这个方法是LinkedHashMap用的,这里先不涉及

      ++modCount;
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
    

    首先代码能走到这里,是③为true或者是⑪为false,这两个是什么意思呢,意思是之前没有相同的key,也就是说HashMap的大小会+1,++modCount暂且不提,我们先看后面的,如果此时size+1后比threshold大(这个变量的值我们在讲resize的时候会重新介绍这里你可以简单以为是 容量*0.75),则会扩容。
    afterNodeInsertion()方法也是LinkedHashMap的方法,这里暂不做介绍,至此Put方法完成啦~~

    本猫的期望就是能够让大家更加容易的学习源码理解源码,诸君如果有建议在下面留言哦~下期会讲resize()方法

    我是加薪猫
    技术是加薪的前提,生活是加薪的意义
    技术等于生活

    相关文章

      网友评论

        本文标题:HashMap源码浅入深出(二)

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