HashMap实现原理以及源码解析jdk1.8(3)--put与扩容

本文详细介绍了HashMap的put过程,包括计算哈希值、处理碰撞、链表与红黑树的转换。在HashMap中,当链表长度达到8且数组长度大于64时,链表会转换为红黑树。扩容机制是在元素达到容量的75%时,将容量扩大为原来的两倍。删除节点时,会根据key和hash值定位到节点,然后进行移除操作,同时调整链表或红黑树结构。
摘要由CSDN通过智能技术生成

HashMap实现原理以及源码解析jdk1.8--put与扩容

 

1、put源码:

先执行put方法


/**
   * Associates the specified value with the specified key in this map.
   * If the map previously contained a mapping for the key, the old
   * value is replaced.
   *
   * @param key   key with which the specified value is to be associated
   * @param value value to be associated with the specified key
   * @return the previous value associated with <tt>key</tt>, or <tt>null</tt> if there was no
   * mapping for <tt>key</tt>. (A <tt>null</tt> return can also indicate that the map previously
   * associated <tt>null</tt> with <tt>key</tt>.)
   */
  /**
   * 将指定参数key和指定参数value插入map中,如果key已经存在,那就替换key对应的value
   * put(K key, V value)可以分为三个步骤:
   * 1.通过hash(Object key)方法计算key的哈希值。
   * 2.通过putVal(hash(key), key, value, false, true)方法实现功能。
   * 3.返回putVal方法返回的结果。
   *
   * @param key   指定key
   * @param value 指定value
   * @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null
   */
  public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
  }

2、put方法中对key进行了hash处理,计算其对应的hash值

  /**
   * HashMap中键值对的存储形式为链表节点,hashCode相同的节点(位于同一个桶)用链表组织
   * hash方法分为三步:
   * 1.取key的hashCode
   * 2.key的hashCode高16位异或低16位
   * 3.将第一步和第二步得到的结果进行取模运算。
   */
  static final int hash(Object key) {
    int h;
    //计算key的hashCode, h = Objects.hashCode(key)
    //h >>> 16表示对h无符号右移16位,高位补0,然后h与h >>> 16按位异或
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

整个过程本质上就是三步:

  1. 拿到 key 的 hashCode 值
  2. 将 hashCode 的高位参与运算,重新计算 hash 值
  3. 将计算出来的 hash 值与 (table.length - 1) 进行 & 运算

注:

如果传入的int类型的值:

向一个Object类型赋值一个int的值时,会将int值自动封箱为Integer。

Integer类型的hashcode都是他自身的值,即h=key;

h >>> 16为无符号右移16位,低位挤走,高位补0;

^ 为按位异或,即转成二进制后,相异为1,相同为0.

由此可发现,当传入的值小于 2的16次方-1 时,调用这个方法返回的值,都是自身的值。

 

4、putVal过程

/**
   * Implements Map.put and related methods
   *
   * @param hash         hash for key
   * @param key          the key
   * @param value        the value to put
   * @param onlyIfAbsent if true, don't change existing value
   * @param evict        if false, the table is in creation mode.
   * @return previous value, or null if none
   */

  /**
   * Map.put和其他相关方法的实现需要的方法
   * putVal方法可以分为下面的几个步骤:
   * 1.如果哈希表为空,调用resize()创建一个哈希表。
   * 2.如果指定参数hash在表中没有对应的桶,即为没有碰撞,直接将键值对插入到哈希表中即可。
   * 3.如果有碰撞,遍历桶,找到key映射的节点
   *    3.1桶中的第一个节点就匹配了,将桶中的第一个节点记录起来。
   *    3.2如果桶中的第一个节点没有匹配,且桶中结构为红黑树,则调用红黑树对应的方法插入键值对。
   *    3.3如果不是红黑树,那么就肯定是链表。遍历链表,如果找到了key映射的节点,就记录这个节点,退出循环。
   *    如果没有找到,在链表尾部插入节点。
   *    插入后,如果链的长度大于TREEIFY_THRESHOLD这个临界值,则使用treeifyBin方法把链表转为红黑树。
   * 4.如果找到了key映射的节点,且节点不为null
   *    4.1记录节点的vlaue。
   *    4.2如果参数onlyIfAbsent为false,或者oldValue为null,替换value,否则不替换。
   *    4.3返回记录下来的节点的value。
   * 5.如果没有找到key映射的节点(2、3步中讲了,这种情况会插入到hashMap中),插入节点后size会加1,
   * 这时要检查size是否大于临界值threshold,如果大于会使用resize方法进行扩容。
   *
   * @param hash         指定参数key的哈希值
   * @param key          指定参数key
   * @param value        指定参数value
   * @param onlyIfAbsent 如果为true,即使指定参数key在map中已经存在,也不会替换value
   * @param evict        如果为false,数组table在创建模式中
   * @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null。
   */

  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                 boolean evict) {
    Node<K, V>[] tab;
    Node<K, V> p;
    int n, i;

    //如果哈希表为空,调用resize()创建一个哈希表,并用变量n记录哈希表长度
    if ((tab = table) == null || (n = tab.length) == 0) {
        /* 这里调用resize,其实就是第一次put时,对数组进行初始化。*/
        n = (tab = resize()).length;
    }

   /**
    * 如果指定参数hash在表中没有对应的桶,即为没有碰撞.
    * (n - 1) & hash 计算key将被放置的槽位.
    * (n - 1) & hash 本质上是hash % n,位运算更快.
    */
    //如果选定的数组坐标处没有元素,直接放入
    if ((p = tab[i = (n - 1) & hash]) == null) {
        //位置为空,将i位置上赋值一个node对象
        tab[i] = newNode(hash, key, value, null);
    } else { // 桶中已经存在元素, 则进入else
      Node<K, V> e;
      K k;

      //如果链表第一个元素或树的根的key与要插入的数元素key 和 hash值完全相同,覆盖旧值
      if (p.hash == hash &&
          ((k = p.key) == key || (key != null && key.equals(k)))) {
        // 将第一个元素赋值给e,用e来记录
        e = p;

      // 当前桶中无该键值对, 进入else. 此时如果桶是红黑树结构,按照红黑树结构插入,调用putTreeVal
      } else if (p instanceof TreeNode) {
        e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);

      //p与新节点既不完全相同,p也不是treenode的实例, 即桶是链表结构,按照链表结构插入到尾部
      //此时只是hash冲突,并且是链表
      } else {
        //遍历链表,找到合适的处理方式 1插入新节点 2覆盖旧值
        for (int binCount = 0; ; ++binCount) { //一个死循环
              //在链表尾部插入新节点
              if ((e = p.next) == null) { //e=p.next,如果p的next指向为null
                  // 先将新节点插入到 p.next
                  p.next = newNode(hash, key, value, null);

                   //如果冲突的节点数已经达到8个,看是否需要改变冲突节点的存储结构,
                   // treeifyBin首先判断当前hashMap的长度,如果不足64,只进行resize,扩容table,
                   // 如果达到64,那么将冲突的存储结构为红黑树
                  if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                  {
                    //将链表转化为二叉树
                    treeifyBin(tab, hash);
                  }
                  break;
              }

              // 如果链表中有一个节点key和新插入的key重复,则跳出循环。
              // 链表节点的<key, value>与put操作<key, value>相同时,不做重复操作,跳出循环
              // 直白一点就是说,如果遍历过程中链表中的元素与新添加的元素完全相同,则跳出循环
              if (e.hash == hash &&
                  ((k = e.key) == key || (key != null && key.equals(k)))) {
                break;
              }

              //将p中的next赋值给p,即将链表中的下一个node赋值给p, 继续循环遍历链表中的元素
              p = e;
        }
      }

      //这个判断中代码作用为:如果添加的元素产生了hash冲突,那么调用put方法时,会将他在链表中他的上一个元素的值返回
      if (e != null) { // existing mapping for key
          // 记录e的value
          V oldValue = e.value;

          //判断条件成立的话,将oldvalue替换
          if (!onlyIfAbsent || oldValue == null) {
              //为newvalue,返回oldvalue;不成立则不替换,然后返回oldvalue
              e.value = value;
          }

          // 访问后回调
          afterNodeAccess(e);

          // 返回旧值
          return oldValue;
      }
    }

    //记录修改次数
    ++modCount;

    // 如果元素数量大于临界值,则进行 rehash 扩容
    // 扩张也是同步判断和执行的,所以恰好触发时会比较慢
    if (++size > threshold) {
      resize();
    }

    // 插入后回调
    afterNodeInsertion(evict);
    return null;
  }

5、putVal执行过程 -- 新元素的插入

  • 1、计算元素的位置。hash&(n-1)就是新元素的位置,n代表的是table长度。hash(key) 得到元素位置。

  • 2、查看对应位置是否有元素。

  • 3、如果没有,直接插入;如果有,进行hash碰撞的处理。

  • 4、hash碰撞三种情况

  1. 一是碰撞的元素与要插入的元素hash值和key都相等,直接进行value的更新;

  2. 二是结点类型是树结点直接,进行树结点的增加或更新;

  3. 三是普通结点,只是hash碰撞,key不相同,于是增长链表。

  • 5、插入之后,会进行hashmap大小的判断,如果元素数量超过了threshold,则会进行resize()扩容。

 

每当插入一个元素的时候,就会对这个元素的键的Hash值按此时的数组长度取模,然后装入对应的位置。比如一个hash值为14的元素插入一个table长度为16(源码中为DEFAULT_INITIAL_CAPACITY默认值)的hashMap中,14对16取模是14(源码中为(n - 1) & hash, 即 (16 - 1) & 14=14),于是就装入14这个位置。不同的元素取模之后发生碰撞,比如30对16取模也等于14,这样也需要放入14的位置,于是就会在这个桶中形成链表

 

hashmap-set.png

 

6、桶由 链表--》红黑树(树化),红黑树--》链表(树链化)

  • 随着数据的不断增加,数组(碰撞导致)和链表(数据新增)长度一直增加,当链表长度超过8,且数组长度不超过64时,链表会被扩容
  • 树化的条件是:桶中链表的长度达到了8,并且数组的长度大于等于64,链表就会被树化成为红黑树结构
  • 在极端情况下: 当连续存储的元素的 hash 相同, 个数达到 11时, 也就是说 table 中只有一个元素, 但是链表长度达到 11, 此时链表也会转树形。
  • 随着链表的变短,存储结构又会从红黑树转变为链表。
  • 为什么要转换?  因为Map中桶的元素初始化是链表保存的,其查找性能是O(n),而树结构能将查找性能提升到O(log(n))。
  • TreeNodes(红黑树)占用空间是普通Nodes(链表)的两倍,为了时间和空间的权衡。
  • 链表变成红黑树也只是当前桶挂载的bin会进行转换,不会影响其它桶的数据结构。
  • 转为红黑树节点后,链表的结构还存在,通过 next 属性维持,红黑树节点在进行操作时都会维护链表的结构,并不是转为红黑树节点,链表结构就不存在了。
  • 在红黑树上,叶子节点也可能有 next 节点,因为红黑树的结构跟链表的结构是互不影响的,不会因为是叶子节点就说该节点已经没有 next 节点。

红黑树


hashmap 红黑树.jpg
`

 单个桶链表长度达到8时,其他桶都是为空,桶的结构还是为链表,数组长度为16。

 单个桶链表长度达到9时,其他桶还都是为空,桶的结构还是为链表,数组长度为32,扩容了。 

 

 单个桶链表长度达到10时,其他桶都是为空,桶的结构还是为链表,数组长度为64。 

 

 单个桶链表长度达到11时,其他桶都是为空,桶的结构由链表转变为红黑树(树化),数组长度为64。 

 

 单个桶链表长度达到15时,其他桶都是为空,桶的结构是红黑树,插入新的key,插入到树结构中,数组长度为64。

 

开始移除节点

 当移除节点,桶的大小为10,结构还是红黑树。 

 继续移除节点,桶的大小为6,结构还是红黑树。

 当移除节点,桶的大小为5,结构由红黑树转变为链表(红黑树--》链表(树链化))。

插入新的key,放到其他的桶中,桶与桶之间结构互不影响,一个桶是红黑树,另外一个桶可以时链表。

 

    /**
     * 将链表转化为红黑树
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; 
        Node<K,V> e;
        
        // 如果桶数组table为空,或者桶数组table的长度小于MIN_TREEIFY_CAPACITY,不符合转化为红黑树的条件
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            //扩容
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            // 如果符合转化为红黑树的条件,而且hash对应的桶不为null, 则重新计算 hash段位,及table的索引位,第一个节点
            
            /************ 双向链表 start***************/
            // 红黑树的hd头节点, tl尾节点
            TreeNode<K,V> hd = null, tl = null;
            // 遍历链表
            do {
                //替换链表node为树node,建立双向链表
                TreeNode<K, V> p = replacementTreeNode(e, null);
                // 确定树头节点
                if (tl == null) {
                    hd = p;
                } else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);

            /************ 双向链表 end***************/
            // 前面仅仅转换为双向链表,treeify才是转换红黑树的处理方法入口 
            // 第一个节点赋值为头节点,也就是根节点
            if ((tab[index] = hd) != null) {
                // 将二叉树转换为红黑树
                hd.treeify(tab);
            }
        }
    }

 

HashMap本来是以空间换时间,所以填充比(static final float DEFAULT_LOAD_FACTOR = 0.75f;)没必要太大。但是填充比太小又会导致空间浪费。如果关注内存,填充比可以稍大,如果主要关注查找性能,填充比可以稍小。

流程图总结:
hashmap-put流程图.png

 

7、HashMap扩容机制

HashMap扩容可以分为三种情况:

  • 第一种:使用默认构造方法初始化HashMap(HashMap())。从前文可以知道HashMap在一开始初始化的时候会返回一个空的table,并且thershold未赋值所以为0。
    此时oldCap=0, oldThr=thershold=0, 因此第一次扩容的容量为默认值DEFAULT_INITIAL_CAPACITY也就是16。同时threshold = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12。

源代码部分

} else {  // zero initial threshold signifies using defaults
  newCap = DEFAULT_INITIAL_CAPACITY;
  //  默认 16 * 0.75 = 12
  newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
  • 第二种:指定初始容量的构造方法初始化HashMap(HashMap(int initialCapacity)以及HashMap(int initialCapacity, float loadFactor))。那么从下面源码可以看到初始容量会等于threshold,接着 threshold = 当前的容量(threshold)*DEFAULT_LOAD_FACTOR。此时oldCap=0, oldThr=thershold>0。
    源代码执行部分

    else if (oldThr > 0) // initial capacity was placed in threshold
    {
       newCap = oldThr;
    } else {
    
  • 第三种:HashMap不是第一次扩容。如果HashMap已经扩容过的话,那么每次table的容量以及threshold量为原有的两倍。此时入参为Map测构造方法也可以出发扩容动作(HashMap(Map<? extends K, ? extends V> m)).
    源代码执行部分

    if (oldCap > 0) {
        ......
    }
    

HashMap是先插入数据再进行扩容的,但是如果是刚刚初始化容器的时候是先扩容再插入数据。
构造HashMap表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到(填充比0.75*Node.length)时,进行扩容操作,重新调整HashMap大小变为原来2倍大小。扩容很耗时。

/**
   * Initializes or doubles table size.  If null, allocates in
   * accord with initial capacity target held in field threshold.
   * Otherwise, because we are using power-of-two expansion, the
   * elements from each bin must either stay at same index, or move
   * with a power of two offset in the new table.
   *
   * @return the table
   */
  /**
   * 对table进行初始化或者扩容。
   * 如果table为null,则对table进行初始化
   * 如果对table扩容,因为每次扩容都是翻倍,与原来计算(n-1)&hash的结果相比,
   * 节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。
   *
   * resize的步骤总结为:
   * 1.计算扩容后的容量,临界值。
   * 2.将hashMap的临界值修改为扩容后的临界值
   * 3.根据扩容后的容量新建数组,然后将hashMap的table的引用指向新数组。
   * 4.将旧数组的元素复制到table中。
   *
   * @return the table
   */
  final Node<K, V>[] resize() {
    //新建oldTab数组保存扩容前的数组table
    Node<K, V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //默认构造器的情况下为0
    int oldThr = threshold;
    int newCap, newThr = 0;

    //如果旧表的长度不是空, 扩容肯定执行这个分支
    if (oldCap > 0) {
        //当前table容量大于最大值得时候返回当前table. 此时loadfactor很大,冲突量比较大,查询不再是O(1)
        if (oldCap >= MAXIMUM_CAPACITY) {
          threshold = Integer.MAX_VALUE;
          return oldTab;

        // 把新表的长度设置为旧表长度的两倍,newCap=2*oldCap
        } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
            oldCap >= DEFAULT_INITIAL_CAPACITY) {
          //扩容容量为2倍,临界值为2倍
          newThr = oldThr << 1; // double threshold
        }

    // 如果旧表的长度的是0,就是说第一次初始化表
    } else if (oldThr > 0) // initial capacity was placed in threshold
    {
      //使用带有初始容量的构造器时,table容量为初始化得到的threshold
      newCap = oldThr;
    } else {               // zero initial threshold signifies using defaults
        //默认构造器下进行扩容
        newCap = DEFAULT_INITIAL_CAPACITY;
        //  默认 16 * 0.75 = 12
        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
    threshold = newThr;

    @SuppressWarnings({"rawtypes", "unchecked"})
    // 开始构造新表, 按newCap创建新的数组,初始化表中的数据
    Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
    //新的数组赋值给table
    table = newTab;

    //扩容后,对新扩容后的table赋值,重新计算元素新的位置。 原表不是空要把原表中数据移动到新表中
    if (oldTab != null) {   // oldCap 原数组
      //遍历原来的旧表
      for (int j = 0; j < oldCap; ++j) {
        Node<K, V> e;

        //判断当前遍历下的该node是否为空,将j位置上的节点保存到e, 然后将oldTab[j]置为空。
        if ((e = oldTab[j]) != null) {
          // 为什么要置为空,有什么好处?? TODO
          oldTab[j] = null;

          ///普通节点, 位置是hash求余。 如果为null 说明这个node没有链表直接放在新表的e.hash & (newCap - 1)位置
          if (e.next == null) {
            newTab[e.hash & (newCap - 1)] = e;
          } else if (e instanceof TreeNode) {
              //  树形结构修剪. 当扩容时,
              //  如果当前桶中元素结构是红黑树,并且元素个数小于链表还原阈值 UNTREEIFY_THRESHOLD (默认为 6),就会把桶中的树形结构缩小或者直接还原(切分)为链表结构
            ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);

          // 如果e后边有链表,到这里表示e后面带着个单链表,需要遍历单链表,将每个结点重新计算在新表的位置,并进行搬运
          } else { // preserve order 保证顺序
              Node<K, V> loHead = null, loTail = null;
              Node<K, V> hiHead = null, hiTail = null;
              Node<K, V> next;

              /*
              这里如果判断成立,那么该元素的地址在新的数组中就不会改变。
              因为oldCap的最高位的1,在e.hash对应的位上为0,所以扩容后得到的地址是一样的,位置不会改变 ,在后面的代码的执行中会放到loHead中去,最后赋值给newTab[j];

              如果判断不成立,那么该元素的地址变为 原下标位置+oldCap,也就是lodCap最高位的1,在e.hash对应的位置上也为1,所以扩容后的地址改变了,在后面的代码中会放到hiHead中,最后赋值给newTab[j + oldCap]

              举个例子来说一下上面的两种情况:
                设:oldCap=16 二进制为:0001 0000
                   oldCap-1=15 二进制为:0000 1111
                   e1.hash=10 二进制为:0000 1010
                   e2.hash=26 二进制为:0101 1010
                e1在扩容前的位置为:e1.hash & oldCap-1  结果为:0000 1010
                e2在扩容前的位置为:e2.hash & oldCap-1  结果为:0000 1010
                结果相同,所以e1和e2在扩容前在同一个链表上,这是扩容之前的状态。

                现在扩容后,需要重新计算元素的位置,在扩容前的链表中计算地址的方式为e.hash & oldCap-1
                那么在扩容后应该也这么计算,扩容后的容量为oldCap*2=32,2^5, 二进制为:0010 0000。 所以 newCap=32,
                新的计算方式应该为
                e1.hash & newCap-1
                即:0000 1010 & 0001 1111
                结果为0000 1010与扩容前的位置完全一样。
                e2.hash & newCap-1 即:0101 1010 & 0001 1111
                结果为0001 1010,为扩容前位置+oldCap。

                而这里却没有e.hash & newCap-1 而是 e.hash & oldCap,其实这两个是等效的,都是判断倒数第五位是0,还是1。
                如果是0,则位置不变,是1则位置改变为扩容前位置+oldCap。

                再来分析下loTail, loHead这两个的执行过程(假设(e.hash & oldCap) == 0成立):
                第一次执行:
                e指向oldTab[j]所指向的node对象,即e指向该位置上链表的第一个元素.
                loTail为空,所以loHead指向与e相同的node对象(loHead = e;),然后loTail也指向了同一个node对象(loTail = e;)。
                最后,在判断条件e指向next,就是指向oldTab链表中的第二个元素

                第二次执行:
                lotail不为null,所以lotail.next指向e,这里其实是lotail指向的node对象的next指向e,
                也可以说是,loHead的next指向了e,就是指向了oldTab链表中第二个元素。此时loHead指向
                的node变成了一个长度为2的链表。然后lotail=e也就是指向了链表中第二个元素的地址。

                第三次执行:
                与第二次执行类似,loHead上的链表长度变为3,又增加了一个node,loTail指向新增的node......

                hiTail与hiHead的执行过程与以上相同。
                由此可以看出,loHead是用来保存新链表上的头元素的,loTail是用来保存尾元素的,直到遍历完链表。
                这是(e.hash & oldCap) == 0成立的时候。

                (e.hash & oldCap) == 0不成立的情况也相同,其实就是把oldCap遍历成两个新的链表,
                通过loHead和hiHead来保存链表的头结点,然后将两个头结点放到newTab[j]与newTab[j+oldCap]上面去。
              */
              do {
                //记录下一个结点
                next = e.next;

                // 新表是旧表的两倍容量,实例上就把单链表拆分为两队, e.hash&oldCap==0为偶数一队,反之为奇数一对
                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);

              //lo队不为null,放在新表原位置
              if (loTail != null) {
                //尾节点的next设置为空
                loTail.next = null;
                newTab[j] = loHead;
              }

              //hi队不为null,放在新表j+oldCap位置
              if (hiTail != null) {
                //尾节点的next设置为空
                hiTail.next = null;
                newTab[j + oldCap] = hiHead;
              }
          }
        }
      }
    }
    return newTab;
  }

8、链表中移除一个节点只需如下图操作,其他操作同理。

9、红黑树在维护链表结构时,移除一个节点只需如下图操作(红黑树中增加了一个 prev 属性),其他操作同理。注:此处只是红黑树维护链表结构的操作,红黑树还需要单独进行红黑树的移除或者其他操作。

10、源码中进行红黑树的查找时,会反复用到以下两条规则:1)如果目标节点的 hash 值小于 p 节点的 hash 值,则向 p 节点的左边遍历;否则向 p 节点的右边遍历。2)如果目标节点的 key 值小于 p 节点的 key 值,则向 p 节点的左边遍历;否则向 p 节点的右边遍历。这两条规则是利用了红黑树的特性(左节点 < 根节点 < 右节点)。

 

 11、remove 方法

/**
 * 移除某个节点
 */
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}
 
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 1.如果table不为空并且根据hash值计算出来的索引位置不为空, 将该位置的节点赋值给p
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        // 2.如果p的hash值和key都与入参的相同, 则p即为目标节点, 赋值给node
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            // 3.否则将p.next赋值给e,向下遍历节点
            // 3.1 如果p是TreeNode则调用红黑树的方法查找节点
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                // 3.2 否则,进行普通链表节点的查找
                do {
                    // 当节点的hash值和key与传入的相同,则该节点即为目标节点
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;	// 赋值给node, 并跳出循环
                        break;
                    }
                    p = e;  // p节点赋值为本次结束的e,在下一次循环中,e为p的next节点
                } while ((e = e.next) != null); // e指向下一个节点
            }
        }
        // 4.如果node不为空(即根据传入key和hash值查找到目标节点),则进行移除操作
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            // 4.1 如果是TreeNode则调用红黑树的移除方法
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            // 4.2 如果node是该索引位置的头节点则直接将该索引位置的值赋值为node的next节点,
            // “node == p”只会出现在node是头节点的时候,如果node不是头节点,则node为p的next节点
            else if (node == p)
                tab[index] = node.next;
            // 4.3 否则将node的上一个节点的next属性设置为node的next节点,
            // 即将node节点移除, 将node的上下节点进行关联(链表的移除)
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node); // 供LinkedHashMap使用
            // 5.返回被移除的节点
            return node;
        }
    }
    return null;
}

removeTreeNode:移除调用此方法的节点,也就是该方法中的 this 节点。移除包括链表的处理和红黑树的处理。

/**
 * 红黑树的节点移除
 */
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                          boolean movable) {
    // --- 链表的处理start ---
    int n;
    // 1.table为空或者length为0直接返回
    if (tab == null || (n = tab.length) == 0)
        return;
    // 2.根据hash计算出索引的位置
    int index = (n - 1) & hash;
    // 3.将索引位置的头节点赋值给first和root
    TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
    // 4.该方法被将要被移除的node(TreeNode)调用, 因此此方法的this为要被移除node节点,
    // 将node的next节点赋值给succ节点,prev节点赋值给pred节点
    TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
    // 5.如果pred节点为空,则代表要被移除的node节点为头节点,
    // 则将table索引位置的值和first节点的值赋值为succ节点(node的next节点)即可
    if (pred == null)
        tab[index] = first = succ;
    else
        // 6.否则将pred节点的next属性设置为succ节点(node的next节点)
        pred.next = succ;
    // 7.如果succ节点不为空,则将succ的prev节点设置为pred, 与前面对应
    if (succ != null)
        succ.prev = pred;
    // 8.如果进行到此first节点为空,则代表该索引位置已经没有节点则直接返回
    if (first == null)
        return;
    // 9.如果root的父节点不为空, 则将root赋值为根节点
    if (root.parent != null)
        root = root.root();
    // 10.通过root节点来判断此红黑树是否太小, 如果是则调用untreeify方法转为链表节点并返回
    // (转链表后就无需再进行下面的红黑树处理)
    if (root == null || root.right == null ||
        (rl = root.left) == null || rl.left == null) {
        tab[index] = first.untreeify(map);  // too small
        return;
    }
    // --- 链表的处理end ---
 
    // --- 以下代码为红黑树的处理 ---
    // 11.将p赋值为要被移除的node节点,pl赋值为p的左节点,pr赋值为p 的右节点
    TreeNode<K,V> p = this, pl = left, pr = right, replacement;
    // 12.如果p的左节点和右节点都不为空时
    if (pl != null && pr != null) {
        // 12.1 将s节点赋值为p的右节点
        TreeNode<K,V> s = pr, sl;
        // 12.2 向左一直查找,跳出循环时,s为没有左节点的节点
        while ((sl = s.left) != null)
            s = sl;
        // 12.3 交换p节点和s节点的颜色
        boolean c = s.red; s.red = p.red; p.red = c;
        TreeNode<K,V> sr = s.right; // s的右节点
        TreeNode<K,V> pp = p.parent;    // p的父节点
        // --- 第一次调整和第二次调整:将p节点和s节点进行了位置调换 ---
        // 12.4 第一次调整
        // 如果p节点的右节点即为s节点,则将p的父节点赋值为s,将s的右节点赋值为p
        if (s == pr) {
            p.parent = s;
            s.right = p;
        }
        else {
            // 将sp赋值为s的父节点
            TreeNode<K,V> sp = s.parent;
            // 将p的父节点赋值为sp
            if ((p.parent = sp) != null) {
                // 如果s节点为sp的左节点,则将sp的左节点赋值为p节点
                if (s == sp.left)
                    sp.left = p;
                // 否则s节点为sp的右节点,则将sp的右节点赋值为p节点
                else
                    sp.right = p;
            }
            // s的右节点赋值为p节点的右节点
            if ((s.right = pr) != null)
                // 如果pr不为空,则将pr的父节点赋值为s
                pr.parent = s;
        }
        // 12.5 第二次调整
        // 将p的左节点赋值为空,pl已经保存了该节点
        p.left = null;
        // 将p节点的右节点赋值为sr,如果sr不为空,则将sr的父节点赋值为p节点
        if ((p.right = sr) != null)
            sr.parent = p;
        // 将s节点的左节点赋值为pl,如果pl不为空,则将pl的父节点赋值为s节点
        if ((s.left = pl) != null)
            pl.parent = s;
        // 将s的父节点赋值为p的父节点pp
        // 如果pp为空,则p节点为root节点, 交换后s成为新的root节点
        if ((s.parent = pp) == null)
            root = s;
        // 如果p不为root节点, 并且p是pp的左节点,则将pp的左节点赋值为s节点
        else if (p == pp.left)
            pp.left = s;
        // 如果p不为root节点, 并且p是pp的右节点,则将pp的右节点赋值为s节点
        else
            pp.right = s;
        // 12.6 寻找replacement节点,用来替换掉p节点
        // 12.6.1 如果sr不为空,则replacement节点为sr,因为s没有左节点,所以使用s的右节点来替换p的位置
        if (sr != null)
            replacement = sr;
        // 12.6.1 如果sr为空,则s为叶子节点,replacement为p本身,只需要将p节点直接去除即可
        else
            replacement = p;
    }
    // 13.承接12点的判断,如果p的左节点不为空,右节点为空,replacement节点为p的左节点
    else if (pl != null)
        replacement = pl;
    // 14.如果p的右节点不为空,左节点为空,replacement节点为p的右节点
    else if (pr != null)
        replacement = pr;
    // 15.如果p的左右节点都为空, 即p为叶子节点, replacement节点为p节点本身
    else
        replacement = p;
    // 16.第三次调整:使用replacement节点替换掉p节点的位置,将p节点移除
    if (replacement != p) { // 如果p节点不是叶子节点
        // 16.1 将p节点的父节点赋值给replacement节点的父节点, 同时赋值给pp节点
        TreeNode<K,V> pp = replacement.parent = p.parent;
        // 16.2 如果p没有父节点, 即p为root节点,则将root节点赋值为replacement节点即可
        if (pp == null)
            root = replacement;
        // 16.3 如果p不是root节点, 并且p为pp的左节点,则将pp的左节点赋值为替换节点replacement
        else if (p == pp.left)
            pp.left = replacement;
        // 16.4 如果p不是root节点, 并且p为pp的右节点,则将pp的右节点赋值为替换节点replacement
        else
            pp.right = replacement;
        // 16.5 p节点的位置已经被完整的替换为replacement, 将p节点清空, 以便垃圾收集器回收
        p.left = p.right = p.parent = null;
    }
    // 17.如果p节点不为红色则进行红黑树删除平衡调整
    // (如果删除的节点是红色则不会破坏红黑树的平衡无需调整)
    TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
 
    // 18.如果p节点为叶子节点, 则简单的将p节点去除即可
    if (replacement == p) {
        TreeNode<K,V> pp = p.parent;
        // 18.1 将p的parent属性设置为空
        p.parent = null;
        if (pp != null) {
            // 18.2 如果p节点为父节点的左节点,则将父节点的左节点赋值为空
            if (p == pp.left)
                pp.left = null;
            // 18.3 如果p节点为父节点的右节点, 则将父节点的右节点赋值为空
            else if (p == pp.right)
                pp.right = null;
        }
    }
    if (movable)
        // 19.将root节点移到索引位置的头节点
        moveRootToFront(tab, r);
}

图解:removeTreeNode 图解

下面的图解是代码中的最复杂的情况,即流程最长的那个,p 节点不为根节点,p 节点有左右节点,s 节点不为 pr 节点,s 节点有右节点。

另外,第一次调整和第二次调整的是本人根据代码而设定的,将第一次调整和第二次调整合起来看会更容易理解,如下:

  • 第一次调整 + 第二次调整:将 p 节点和 s 节点进行了位置调换,选出要替换掉 p 节点的 replacement
  • 第三次调整:将 replacement 节点覆盖掉 p 节点

 

参考资料:

http://blog.csdn.net/v123411739/article/details/78996181

 

 

每天努力一点,每天都在进步。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

powerfuler

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值