前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >HashMap 源码解析-终章

HashMap 源码解析-终章

作者头像
热心的大肚皮
发布2023-02-28 13:39:41
2730
发布2023-02-28 13:39:41
举报
文章被收录于专栏:程序猿日常笔记

七、HashMap 扩容方法 resize()

resize() 方法中比较重要的是链表和红黑树的 rehash 操作,先来说下 rehash 的实现原理:

  我们在扩容的时候,一般是把长度扩为原来2倍,所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。

  元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

因此,我们在扩充HashMap的时候,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:

这个算法很巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的槽中了。

不多说直接来源码。

代码语言:javascript
复制
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        
        /**
         *  resize()函数在size > threshold时被调用。
         *  oldCap大于 0 代表原来的 table 表非空, oldCap 为原表的大小,
         *  oldThr(threshold) 为 oldCap × load_factor
         */
        if (oldCap > 0) {
            //当前table容量大于最大值得时候返回当前table
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //table的容量乘以2,threshold的值也乘以2
                newThr = oldThr << 1; // double threshold
        }
     /**
         *  resize()函数在table为空被调用。
         *  oldCap 小于等于 0 且 oldThr 大于0,代表用户创建了一个 HashMap,但是使用的构造函数为
         *  HashMap(int initialCapacity, float loadFactor) 或 HashMap(int initialCapacity)
         *  或 HashMap(Map<? extends K, ? extends V> m),导致 oldTab 为 null,oldCap 为0,
         *  oldThr 为用户指定的 HashMap的初始容量。
         */
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
     /**
         *  resize()函数在table为空被调用。
         *  oldCap 小于等于 0 且 oldThr 等于0,用户调用 HashMap()构造函数创建的 HashMap,所有值均采用默认值,
         *  oldTab(Table)表为空,oldCap为0,oldThr等于0,
         */
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;        
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
       //把 oldTab 中的节点 reHash 到 newTab 中去
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {  
                    oldTab[j] = null;  // 将索引值为j的老表头节点赋值给e
            //如果e.next为空, 则代表老表的该位置只有1个节点,计算新表的索引位置, 直接将该节点放在该位置
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
            //若节点是 TreeNode 节点,要进行 红黑树的 rehash 操作
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
            //若是链表,进行链表的 rehash 操作
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点
                        Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引位置+oldCap”的节点
                        Node<K,V> next;
                        do {
                              next = e.next;
                          // 如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
                          //此处为代码精辟位置,为计算元素的新索引位置,在扩容前计算元素位置时,
                          //为e.hash &(oldCap-1),oldCap是2的n次幂,而e.hash & oldCap与原来的区别
                          //则是与高位的1,以此来判断元素在新的hash表里的位置,如果为0则位置不变
                          //否则则位置发生变化,变化了2的n次幂,也就是一个oldCap的长度
                          if ((e.hash & oldCap) == 0) {
                                if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点
                                    loHead = e;     // 则将loHead赋值为第一个节点
                                else
                                    loTail.next = e; // 否则将节点添加在loTail后面
                                loTail = e; // 并将loTail赋值为新增的节点
                           }
                           // 如果e的hash值与老表的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+oldCap
                           else {
                                if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点
                                    hiHead = e; // 则将hiHead赋值为第一个节点
                                else
                                    hiTail.next = e;    // 否则将节点添加在hiTail后面
                                hiTail = e; // 并将hiTail赋值为新增的节点
                           }
                        } while ((e = next) != null);
                        // 如果loTail不为空(说明老表的数据有分布到新表上“原索引位置”的节点),则将最后一个节点
                        // 的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 如果hiTail不为空(说明老表的数据有分布到新表上“原索引+oldCap位置”的节点),则将最后
                        // 一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

额外扩展

代码语言:javascript
复制
/**
   * 扩容后,红黑树的hash分布,只可能存在于两个位置:原索引位置、原索引位置+oldCap
   */
  final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
      TreeNode<K,V> b = this;  // 拿到调用此方法的节点
      TreeNode<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点
      TreeNode<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引+oldCap”的节点
      int lc = 0, hc = 0;
      // 1.以调用此方法的节点开始,遍历整个红黑树节点
      for (TreeNode<K,V> e = b, next; e != null; e = next) {  // 从b节点开始遍历
          next = (TreeNode<K,V>)e.next;   // next赋值为e的下个节点
          e.next = null;  // 同时将老表的节点设置为空,以便垃圾收集器回收
          // 2.如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
          if ((e.hash & bit) == 0) {
              if ((e.prev = loTail) == null)  // 如果loTail为空, 代表该节点为第一个节点
                  loHead = e; // 则将loHead赋值为第一个节点
              else
                  loTail.next = e;    // 否则将节点添加在loTail后面
              loTail = e; // 并将loTail赋值为新增的节点
              ++lc;   // 统计原索引位置的节点个数
          }
          // 3.如果e的hash值与老表的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+oldCap
          else {
              if ((e.prev = hiTail) == null)  // 如果hiHead为空, 代表该节点为第一个节点
                  hiHead = e; // 则将hiHead赋值为第一个节点
              else
                  hiTail.next = e;    // 否则将节点添加在hiTail后面
              hiTail = e; // 并将hiTail赋值为新增的节点
              ++hc;   // 统计索引位置为原索引+oldCap的节点个数
          }
      }
      // 4.如果原索引位置的节点不为空
      if (loHead != null) {   // 原索引位置的节点不为空
          // 4.1 如果节点个数<=6个则将红黑树转为链表结构
          if (lc <= UNTREEIFY_THRESHOLD)
              tab[index] = loHead.untreeify(map);
          else {
              // 4.2 将原索引位置的节点设置为对应的头节点
              tab[index] = loHead;
              // 4.3 如果hiHead不为空,则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)
              // 已经被改变, 需要重新构建新的红黑树
              if (hiHead != null)
                  // 4.4 以loHead为根节点, 构建新的红黑树
                  loHead.treeify(tab);
          }
      }
      // 5.如果索引位置为原索引+oldCap的节点不为空
      if (hiHead != null) {   // 索引位置为原索引+oldCap的节点不为空
          // 5.1 如果节点个数<=6个则将红黑树转为链表结构
          if (hc <= UNTREEIFY_THRESHOLD)
              tab[index + bit] = hiHead.untreeify(map);
          else {
              // 5.2 将索引位置为原索引+oldCap的节点设置为对应的头节点
              tab[index + bit] = hiHead;
              // 5.3 loHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)
              // 已经被改变, 需要重新构建新的红黑树
              if (loHead != null)
                  // 5.4 以hiHead为根节点, 构建新的红黑树
                  hiHead.treeify(tab);
          }
      }
  }
  
  
  /**
   * 构建红黑树
   */
  final void treeify(Node<K,V>[] tab) {
      TreeNode<K,V> root = null;
      // 1.将调用此方法的节点赋值给x,以x作为起点,开始进行遍历
      for (TreeNode<K,V> x = this, next; x != null; x = next) {
          next = (TreeNode<K,V>)x.next;   // next赋值为x的下个节点
          x.left = x.right = null;    // 将x的左右节点设置为空
          // 2.如果还没有根节点, 则将x设置为根节点
          if (root == null) {
              x.parent = null;    // 根节点没有父节点
              x.red = false;  // 根节点必须为黑色
              root = x;   // 将x设置为根节点
          }
          else {
              K k = x.key;  // k赋值为x的key
              int h = x.hash;  // h赋值为x的hash值
              Class<?> kc = null;
              // 3.如果当前节点x不是根节点, 则从根节点开始查找属于该节点的位置
              for (TreeNode<K,V> p = root;;) {
                  int dir, ph;
                  K pk = p.key;
                  // 4.如果x节点的hash值小于p节点的hash值,则将dir赋值为-1, 代表向p的左边查找
                  if ((ph = p.hash) > h)
                      dir = -1;
                  // 5.如果x节点的hash值大于p节点的hash值,则将dir赋值为1, 代表向p的右边查找
                  else if (ph < h)
                      dir = 1;
                  // 6.走到这代表x的hash值和p的hash值相等,则比较key值
                  else if ((kc == null &&                                // 6.1 如果k没有实现Comparable接口 或者 x节点的key和p节点的key相等
                            (kc = comparableClassFor(k)) == null) ||
                           (dir = compareComparables(kc, k, pk)) == 0)
                      // 6.2 使用定义的一套规则来比较x节点和p节点的大小,用来决定向左还是向右查找
                      dir = tieBreakOrder(k, pk);
   
                  TreeNode<K,V> xp = p;   // xp赋值为x的父节点,中间变量用于下面给x的父节点赋值
                  // 7.dir<=0则向p左边查找,否则向p右边查找,如果为null,则代表该位置即为x的目标位置
                  if ((p = (dir <= 0) ? p.left : p.right) == null) {
                      // 8.x和xp节点的属性设置
                      x.parent = xp;  // x的父节点即为最后一次遍历的p节点
                      if (dir <= 0)   // 如果时dir <= 0, 则代表x节点为父节点的左节点
                          xp.left = x;
                      else    // 如果时dir > 0, 则代表x节点为父节点的右节点
                          xp.right = x;
                      // 9.进行红黑树的插入平衡(通过左旋、右旋和改变节点颜色来保证当前树符合红黑树的要求)
                      root = balanceInsertion(root, x);
                      break;
                  }
              }
          }
      }
      // 10.如果root节点不在table索引位置的头节点, 则将其调整为头节点
      moveRootToFront(tab, root);
  }
  
  /**
   * 将红黑树节点转为链表节点, 当节点<=6个时会被触发
   */
  final Node<K,V> untreeify(HashMap<K,V> map) {
      Node<K,V> hd = null, tl = null; // hd指向头节点, tl指向尾节点
      // 1.从调用该方法的节点, 即链表的头节点开始遍历, 将所有节点全转为链表节点
      for (Node<K,V> q = this; q != null; q = q.next) {
          // 2.调用replacementNode方法构建链表节点
          Node<K,V> p = map.replacementNode(q, null);
          // 3.如果tl为null, 则代表当前节点为第一个节点, 将hd赋值为该节点
          if (tl == null)
              hd = p;
          // 4.否则, 将尾节点的next属性设置为当前节点p
          else
              tl.next = p;
          tl = p; // 5.每次都将tl节点指向当前节点, 即尾节点
      }
      // 6.返回转换后的链表的头节点
      return hd;
  }

谢谢大家支持

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-11-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序猿日常笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 七、HashMap 扩容方法 resize()
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档