美文网首页
HashMap源码解析 (HashMap类-put方法)

HashMap源码解析 (HashMap类-put方法)

作者: 七喜丶 | 来源:发表于2021-10-27 09:10 被阅读0次

    添加方法 put( )

    put方法是比较复杂的,实现步骤大致如下:

    1. 先通过hash值计算出key 映射到哪个桶;
    2. 如果桶上没有碰撞冲突,则直接插入;
    3. 如果出现碰撞冲突了,则需要处理冲突:
      a 如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;
      b 否则采用传统的链式方法插入。如果链的长度达到阈值,则把链转变为红黑树;
    4. 如果桶中存在重复的键,则为该键替换新值value
    5. 如果 size大于临界值threshold,则进行扩容;
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    

    说明:

    1. HashMap 只提供了 put 用于添加元素,putVal 方法只是给 put 方法调用的一个方法,并没有提供给用户使用。 所以我们重点看 putVal 方法
    2. 我们可以看到在 putVal 方法中 key 在这里执行了一下 hash 方法,来看一下 hash 方法是如何实现的
    static final int hash(Object key) {
        int h;
        /*
        1)如果key等于null:返回的是0.
        2)如果key不等于null:首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的
            二进制进行按位异或得到最后的hash值
        */
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

    在 putVal 函数中使用到了上述 hash 函数计算数组的下标

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        ...
        if ((p = tab[i = (n - 1) & hash]) == null) // 这里的n表示数组长度16
        ...
    }
    

    计算过程如下所示:

    1. key.hashCode();返回散列值也就是 hashcode,假设随便生成的一个值
    2. n 表示数组初始化的长度是 16
    3. & (按位与运算):运算规则:相同的二进制数位上,都是 1 的时候,结果为 1,否则为0
    4. ^ (按位异或运算):运算规则:相同的二进制数位上,数字相同,结果为 0,不同为 1
    (h = key.hashCode()) ^ (h >>> 16)
    
    
    (n - 1) & hash  n是数组的长度16
    
    1111 1111 1111 1111 1111 0000 1110 1010     h=key.hashCode(),假设随便生成的一个值。
    ========================================================
    
    1111 1111 1111 1111 1111 0000 1110 1010     h   进行无符号右移16
    0000 0000 0000 0000 1111 1111 1111 1111     h >>> 16   ^(按位异或运算)
    ---------------------------------------------------------
    1111 1111 1111 1111 0000 1111 0001 0101     返回计算得到的hash值
    
    ---------------------------------------------------------
    0000 0000 0000 0000 0000 0000 0000 1111     (16 - 1)
    1111 1111 1111 1111 0000 1111 0001 0101     (n - 1) & hash (按位与运算)
    ---------------------------------------------------------
    0000 0000 0000 0000 0000 0000 0000 0101      5索引
    
    =========================================================
    
    假设hashCode值直接和数组长度(n-1)按位与运算
    1111 1111 1111 1111 1111 0000 1110 1010     h=key.hashCode(),假设随便生成的一个值。
    0000 0000 0000 0000 0000 0000 0000 1111     (16 - 1)
    ---------------------------------------------------------
    0000 0000 0000 0000 0000 0000 0000 1010      10索引
    
    再存储一个key计算出hashCode值:假如高位变化很大,低位没有改变
    1001 1001 1001 1111 1111 0000 1110 1010
    0000 0000 0000 0000 0000 0000 0000 1111     (16 - 1)
    ---------------------------------------------------------
    0000 0000 0000 0000 0000 0000 0000 1010      10索引
    

    那我们就看看最重要的putVal( )

    • onlyIfAbsent:如果 true 代表不更改现有的值
    • evict:如果为false表示 table 为创建状态
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        /*
            1)transient Node<K,V>[] table; 表示存储Map集合中元素的数组。
            2)(tab = table) == null 表示将空的table赋值给tab,然后判断tab是否等于null,第一次肯定是null。
            3)(n = tab.length) == 0 表示将数组的长度0赋值给n,然后判断n是否等于0,n等于0,由于if判断使用双或,满足一个即可,则执行代码 n = (tab = resize()).length; 进行数组初始化,并将初始化好的数组长度赋值给n。
            4)执行完n = (tab = resize()).length,数组tab每个空间都是null。
        */
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        /*
            1)i = (n - 1) & hash 表示计算数组的索引赋值给i,即确定元素存放在哪个桶中。
            2)p = tab[i = (n - 1) & hash]表示获取计算出的位置的数据赋值给结点p。
            3) (p = tab[i = (n - 1) & hash]) == null 判断结点位置是否等于null,如果为null,则执行代码:tab[i] = newNode(hash, key, value, null);根据键值对创建新的结点放入该位置的桶中。
            小结:如果当前桶没有哈希碰撞冲突,则直接把键值对插入空间位置。
        */ 
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 创建一个新的结点存入到桶中
            tab[i] = newNode(hash, key, value, null);
        else {
             // 执行else说明tab[i]不等于null,表示这个位置已经有值了
            Node<K,V> e; K k;
            /*
                比较桶中第一个元素(数组中的结点)的hash值和key是否相等
                1)p.hash == hash :p.hash表示原来存在数据的hash值  hash表示后添加数据的hash值 比较两个hash值是否相等。
                     说明:p表示tab[i],即 newNode(hash, key, value, null)方法返回的Node对象。
                        Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
                            return new Node<>(hash, key, value, next);
                        }
                        而在Node类中具有成员变量hash用来记录着之前数据的hash值的。
                 2)(k = p.key) == key :p.key获取原来数据的key赋值给k  key 表示后添加数据的key比较两个key的地址值是否相等。
                 3)key != null && key.equals(k):能够执行到这里说明两个key的地址值不相等,那么先判断后添加的key是否等于null,如果不等于null再调用equals方法判断两个key的内容是否相等。
            */
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                    /*
                        说明:两个元素哈希值相等,并且key的值也相等,将旧的元素整体对象赋值给e,用e来记录
                    */ 
                    e = p;
            // hash值不相等或者key不相等;判断p是否为红黑树结点
            else if (p instanceof TreeNode)
                // 放入树中
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 说明是链表结点
            else {
                /*
                    1)如果是链表的话需要遍历到最后结点然后插入
                    2)采用循环遍历的方式,判断链表中是否有重复的key
                */
                for (int binCount = 0; ; ++binCount) {
                    /*
                        1)e = p.next 获取p的下一个元素赋值给e。
                        2)(e = p.next) == null 判断p.next是否等于null,等于null,说明p没有下一个元素,那么此时到达了链表的尾部,还没有找到重复的key,则说明HashMap没有包含该键,将该键值对插入链表中。
                    */
                    if ((e = p.next) == null) {
                        /*
                            1)创建一个新的结点插入到尾部
                             p.next = newNode(hash, key, value, null);
                             Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
                                    return new Node<>(hash, key, value, next);
                             }
                             注意第四个参数next是null,因为当前元素插入到链表末尾了,那么下一个结点肯定是null。
                             2)这种添加方式也满足链表数据结构的特点,每次向后添加新的元素。
                        */
                        p.next = newNode(hash, key, value, null);
                        /*
                            1)结点添加完成之后判断此时结点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树。
                            2)int binCount = 0 :表示for循环的初始化值。从0开始计数。记录着遍历结点的个数。值是0表示第一个结点,1表示第二个结点。。。。7表示第八个结点,加上数组中的的一个元素,元素个数是9。
                            TREEIFY_THRESHOLD - 1 --》8 - 1 ---》7
                            如果binCount的值是7(加上数组中的的一个元素,元素个数是9)
                            TREEIFY_THRESHOLD - 1也是7,此时转换红黑树。
                        */
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 转换为红黑树
                            treeifyBin(tab, hash);
                        // 跳出循环
                        break;
                    }
                     
                    /*
                        执行到这里说明e = p.next 不是null,不是最后一个元素。继续判断链表中结点的key值与插入的元素的key值是否相等。
                    */
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        // 相等,跳出循环
                        /*
                            要添加的元素和链表中的存在的元素的key相等了,则跳出for循环。不用再继续比较了
                            直接执行下面的if语句去替换去 if (e != null) 
                        */
                        break;
                    /*
                        说明新添加的元素和当前结点不相等,继续查找下一个结点。
                        用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                    */
                    p = e;
                }
            }
            /*
                表示在桶中找到key值、hash值与插入元素相等的结点
                也就是说通过上面的操作找到了重复的键,所以这里就是把该键的值变为新的值,并返回旧值
                这里完成了put方法的修改功能
            */
            if (e != null) { 
                // 记录e的value
                V oldValue = e.value;
                // onlyIfAbsent为false或者旧值为null
                if (!onlyIfAbsent || oldValue == null)
                    // 用新值替换旧值
                    // e.value 表示旧值  value表示新值 
                    e.value = value;
                // 访问后回调
                afterNodeAccess(e);
                // 返回旧值
                return oldValue;
            }
        }
        // 修改记录次数
        ++modCount;
        // 判断实际大小是否大于threshold阈值,如果超过则扩容
        if (++size > threshold)
            resize();
        // 插入后回调
        afterNodeInsertion(evict);
        return null;
    }
    
    

    相关文章

      网友评论

          本文标题:HashMap源码解析 (HashMap类-put方法)

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