HashMap部分源码集解

微信用户1326165651

共 7374字,需浏览 15分钟

 ·

2023-06-05 01:45

        本文将主要分析HashMap的重点方法,包括put(), get(), resize()。并对其重点代码进行解释说明。

      
        
             #
          # 阅读本文需具备基础的知识:
        
      
      
            1. 了解Java基础知识;
      
      
            2. 对HashMap有一定的了解并懂得HashMap中一些基本属性的作用;
      
    





## put 方法集解

      
        public V put(K key, V value) {
      
      
            //计算key的hash值
      
      
            return putVal(hash(key), key, value, false, true);
      
      
        }
      
      
        
          

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; int n, i; // Node数组非空判断,为空则调用resize方法进行初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 通过(n - 1) & hash 的方式计算出key的位置,再对当前位置的元素进行非空判断,为null则直接插入 if ((p = tab[i = (n - 1) & hash]) == null)         tab[i] = newNode(hash, key, value, null); else { Node e; K k; //判断key是否存在,存在则下面逻辑选择性覆盖旧值 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //判断是否为红黑树结构,是的话以红黑树的方式插入 else if (p instanceof TreeNode) e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { // 链表的下一节点为null,将数据插入尾部 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //链表长度 >= 8 则转变为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //如果链表中存在key值相等元素则跳出循环,在下面逻辑选择性覆盖旧值 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 如果key已存在,则根据onlyIfAbsent选择性用新值替换旧值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } // HashMap线程安全的保障 ++modCount; // 当Node数组的长度大于扩容阈值时则进行扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }

## put重点代码发明

1.  为什么用(n - 1) & hash 的方式计算元素下标?

      
        原因:(n - 1) & hash == hash % n, 并且(n - 1) & hash 的效率要高。(n为2的次幂)
      
      
        解释:
      
      
          当n == 8 == 1000, hash == 59 == 111011时。
      
      
           (n - 1) & hash == 11 并且 111011 == 11 == 3,
      
      
           59 % 8 == 3
      
      
          根据位运算规则可知,二进制数中凡是低三位为0的数都可以被100整除,低三位的值即为余数;
      
      
          利用位与运算,将高位置0,低三位置为原值,结果低三位的原值即为余数。
      
      
          所以当n为2的次幂时,(n - 1) & hash 即为 hash % n的值
      
      
    

2. putVal方法中onlyIfAbsent的意义及作用

onlyIfAbsent:          - 如果存在新key和旧key相同,是否用新值覆盖旧值;         - 如果为true,则新值不覆盖旧值。false则进行覆盖。

3. modCount的作用

记录集合被修改的次数。 当操作HashMap时,发现modCount发生改变时进入快速失败,抛出 ConcurrentModificati onException





## resize 方法集解

      
        final Node[] resize() {
      
      
            Node[] oldTab = table;
      
      
            // 获取原哈希表容量;若原哈希表为null,则容量为0
      
      
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
      
      
            // 获取原哈希表扩容门槛
      
      
            int oldThr = threshold;
      
      
            // 初始化新哈希表容量和新哈希表门槛为0
      
      
            int newCap, newThr = 0;
      
      
            // 如果原容量大于0,计算扩容后的容量及新的负载扩容门槛
      
      
            if (oldCap > 0) {
      
      
                //判断原容量是否大于等于HashMap允许的容量最大值 - 2的30次幂
      
      
                if (oldCap >= MAXIMUM_CAPACITY) {
      
      
                    // 如果原容量大于等于允许的最大容量,则把HashMap的扩容门槛设置为Integer允许的最大值
      
      
                    threshold = Integer.MAX_VALUE;
      
      
                    //不再扩容,直接返回
      
      
                    return oldTab;
      
      
                }
      
      
                // 把原Node数组容量扩大为原来的2倍作为新Node数组的容量。
      
      
                // 如果新Node数组的容量小于HashMap所允许的容量的最大值: 2的30次幂,并且原Node数组容量大于等于默认的初始化Node数组容量: 2的4次幂 -> 16,则计算新的扩容门槛
      
      
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
      
      
                    // 新Node数组的扩容门槛为原Node数组的扩容门槛的2倍
      
      
                    newThr = oldThr << 1; // double threshold
      
      
            }
      
      
            else if (oldThr > 0) // initial capacity was placed in threshold
      
      
                // 如果原Node数组容量小于等于0并且原Node数组扩容门槛大于0,新Node数组容量为原Node数组扩容门槛大小
      
      
                newCap = oldThr;
      
      
            else {               // zero initial threshold signifies using defaults
      
      
                // 如果原Node数组容量小于等于0并且原Node数组的扩容门槛小于等于0,则初始化新Node数组容量为默认初始化容量:1 << 4;
      
      
                newCap = DEFAULT_INITIAL_CAPACITY;
      
      
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
      
      
            }
      
      
            // 如果新的扩容门槛等于0,则重新计算新的扩容门槛
      
      
            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"})
      
      
            //初始化新NodeNode数组
      
      
            Node[] newTab = (Node[])new Node[newCap];
      
      
            table = newTab;
      
      
            //旧Node数组不为null,则进行扩容
      
      
            if (oldTab != null) {
      
      
                for (int j = 0; j < oldCap; ++j) {
      
      
                    Node e;
      
      
                    if ((e = oldTab[j]) != null) {
      
      
                        oldTab[j] = null;
      
      
                        if (e.next == null)
      
      
                            // 计算新Map的Node数组元素下标。如果当前Node节点没有后续节点,则直接放入新位置
      
      
                            newTab[e.hash & (newCap - 1)] = e;
      
      
                        // 如果当前Node是红黑树,则对树进行处理
      
      
                        else if (e instanceof TreeNode)
      
      
                            ((TreeNode)e).split(this, newTab, j, oldCap);
      
      
                        // 处理链表
      
      
                        else { // preserve order
      
      
                            Node loHead = null, loTail = null;
      
      
                            Node hiHead = null, hiTail = null;
      
      
                            Node next;
      
      
                            do {
      
      
                                next = e.next;
      
      
                                // (e.hash & oldCap) == 0 元素放在低位区,否则放在高位区
      
      
                                // 低位区:原Node长度的位置;高位区:原Node长度到扩容后Node长度的位置
      
      
                                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);
      
      
                            // 将低位区的尾结点指向null,并把低位区的头结点放入新Node数组的合适位置
      
      
                            if (loTail != null) {
      
      
                                loTail.next = null;
      
      
                                newTab[j] = loHead;
      
      
                            }
      
      
                            // 将高位区的头结点放到新Node数组合适的位置
      
      
                            if (hiTail != null) {
      
      
                                hiTail.next = null;
      
      
                                newTab[j + oldCap] = hiHead;
      
      
                            }
      
      
                        }
      
      
                    }
      
      
                }
      
      
            }
      
      
            return newTab;
      
      
            }
      
    

## resize重点代码发明

  1.  为什么要进行扩容?


        减少hash碰撞,使HashMap中的结构更简单。

2. 为什么要以2的倍数作为扩容的策略?

        保证HashMap的容量都是2的次幂,便于key的位置计算和扩容的容量计算。




## get方法集解

      
        public V get(Object key) {
      
      
            Node e;
      
      
            //获取key的hash值
      
      
            return (e = getNode(hash(key), key)) == null ? null : e.value;
      
      
        }
      
      
        
          
final Node getNode(int hash, Object key) { Node[] tab; Node first, e; int n; K k; //Node数组不为null并且其长度大于0并且key位置部位null,则获取当前key位置的Node,否则返回null if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //当前位置的key和入参key相等,则返回当前Node if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 如果首Node节点为空,直接返回null。否则去链表或红黑树中取值 if ((e = first.next) != null) { //判断是否是红黑树,是的话在树中取值 if (first instanceof TreeNode) return ((TreeNode)first).getTreeNode(hash, key); do {                 //在链表中取值,如果key值相等,则取链表中的当前Node节点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } //返回null return null; }



疑问:

现状:在resize方法中,代码 hiTail.next = null; 表示将高位区的尾结点的next指向null。

问:"这个操作不能将高位区和低位区的Node节点通过指针相连,那么遍历链表时如何遍历到低位区的数据?为什么不将高位区尾节点的next指向低位区的头指针?"


注:

集解 - 引自《本草纲目》,意为收集业内众家释义;

发明 - 引自《本草纲目》,意为笔者独立的见解,有别于他。



浏览 19
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报