源码解析系列:HashMap(2) - 核心方法

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


1. 前言

HashMap(1)中介绍了一些相关的方法的原理和一些重要的参数以及四种构造方法。本文会结和上一篇文章介绍HashMap中几个常用的方法。有些地方调用了红黑树的查找,关于红黑树的方法的解析,以后会单独讲解,目前对于红黑树的了解也没有太多。对于自己了解的方法,会尽可能讲清楚每一步是怎么走的。
第一篇文章:源码解析系列:HashMap(1)
第二篇文章:源码解析系列:HashMap(2)
第三篇文章:源码解析系列:HashMap(3)
第四篇文章:源码解析系列:HashMap(4)
第五篇文章:源码解析系列:HashMap(5)

2. HashMap核心方法

1. get

作用:存入一个键值对

public V get(Object key) {
	//node e
    Node<K,V> e;
    //getNode方法获取key对于的value
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}


//获取结点
final Node<K,V> getNode(int hash, Object key) {
	//tab就是table表,first是table数组的第一个,e是first的下一个
	//n是数组的长度
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //如果table数组不为空并且长度大于0,并且通过(n - 1) & hash的公式计算出来的key的下标是有数据的
    //那么就可以开始进行遍历寻找了
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //如果第一个hash就和我们要查找的key相同并且值也相同,就直接返回第一个结点  
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //如果不是第一个结点,就证明可能是下一个结点,遍历完
        if ((e = first.next) != null) {
        	//如果该节点是树结点,证明树化了
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //否则没有树化就调用链表的查找方法
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

java8也给出了一个可以实现重写的put方法,我们可以自己作为扩展

// Overrides of JDK8 Map extension methods
@Override
 public V getOrDefault(Object key, V defaultValue) {
     Node<K,V> e;
     return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
 }


2. containsKey

作用:判断是否包含了某个key

 public boolean containsKey(Object key) {
 //调用getNode方法看能不能获取到结点
  return getNode(hash(key), key) != null;
 }


3. put

//两个put方法都统一调用了putVal方法进行插入
 public V put(K key, V value) {
 	//如果key相同,那么value会被替换
    return putVal(hash(key), key, value, false, true);
  }
  
 @Override
 public V putIfAbsent(K key, V value) {
 	//如果key相同,那么value不会被替换
     return putVal(hash(key), key, value, true, true);
 }

//putAll方法,其实和使用构造器传入map调用的方法一模一样
public void putAll(Map<? extends K, ? extends V> m) {
        putMapEntries(m, true);
}

  /**
   * Implements Map.put and related methods.
   *
   * @param hash key的hash值
   * @param key 键
   * @param value 值
   * @param onlyIfAbsent 如果是true,表示不可以对相同的key的value进行替换
   * @param evict 如果这个参数是false,表明是创建模式
   * @return previous value, or null if none
   */
  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                 boolean evict) {
      //tab: table
      //p:table的下标开始第一个元素
      //n:table的容量
      //i: hash值经过计算的出来的下标
      Node<K,V>[] tab; Node<K,V> p; int n, i;
      //如果table不是空并且容量不为0,
      if ((tab = table) == null || (n = tab.length) == 0)
      	  //对n进行赋值
          n = (tab = resize()).length;
      //对要插入的key-value计算出来的下标的第一个不是空,证明没有元素
      if ((p = tab[i = (n - 1) & hash]) == null)
      	  //就对该下标进行赋值,把插入的key-value设置成第一个
          tab[i] = newNode(hash, key, value, null);
      else {
      	  //e: 一个辅助结点
      	  //k:p的key
          Node<K,V> e; K k;
          //如果p的hash值和输入的hash相同并且p的key和输入的key一致
          if (p.hash == hash &&
              ((k = p.key) == key || (key != null && key.equals(k))))
              //找到了具有相同的key和hash的结点,这样只能判断能否替换了
              e = p;
          //如果p是树,证明已经树化了并且有结点了
          else if (p instanceof TreeNode)
          	  //这里调用树的插入
              e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
          else {
          	  //不是树也不是第一个结点,就证明是链表了
          	  //binCount:计算结点的个数
              for (int binCount = 0; ; ++binCount) {
                  if ((e = p.next) == null) {
                      //如果达到了结尾,就把新节点赋值给尾部
                      p.next = newNode(hash, key, value, null);
                      //判断是不是大于8,TREEIFY_THRESHOLD = 8,但注意在树化方法treeifyBin中
                      //也会判断table的长度也就是容量是不是小于MIN_TREEIFY_CAPACITY(64),如果不是一样不会树化
                      //同时满足结点个数大于8和table数组长度大于64才会树化
                      if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                      	  //这里binCount >= 7是因为binCount从0开始,可以自己算一遍能得到9的时候开始扩容
                      	  //当然,由于官方给出了长度概率是8的概率极其小,所以在table长度没有达到64之前长度为9的情况其实是很罕见的
                          treeifyBin(tab, hash);
                      break;
                  }
                  //判断如果找到了结点中相同的key和hash,直接break
                  if (e.hash == hash &&
                      ((k = e.key) == key || (key != null && key.equals(k))))
                      break;
                  p = e;
              }
          }
          //如果e不为空,证明map中已经存在了相同key的情况
          if (e != null) { // existing mapping for key
          	  //获取旧的值
              V oldValue = e.value;
              //当onlyIfAbsent 为false或者旧的值是空的时候
              if (!onlyIfAbsent || oldValue == null)
              	  //我们用新的value替换旧的value
                  e.value = value;
              //LinkedHashMap回调函数
              afterNodeAccess(e);
              //返回旧的value
              return oldValue;
          }
      }
      //注意,这里如果是替换,是不会++的,只有插入了才会++
      //修改次数+1
      ++modCount;
      //容量+1,如果容量大于扩容阈值
      if (++size > threshold)
      	  //重新分配空间
          resize();
      //插入之后回调的,看了三个实现的方法,发现都是让LinkedHashMap插入后回调使用的
      afterNodeInsertion(evict);
      return null;
  }


4. resize

作用:在插入删除等操作中,根据结点个数重新分配空间,是树化退化成链表还是链表转化成树。比较重要的一个核心方法

旧结点到新节点的分配规则:下面的源码中对于链表会进行从旧table到新的table的转化。旧的table中的结点会被分配到新的table中去。由于新的容量是旧的容量的2倍,计算公式:tab[i = (n - 1) & hash]。比如旧的容量是16,0000 0000 0000 0000 0000 0000 0001 0000,那么新的容量是32,也就是0000 0000 0000 0000 0000 0000 0010 0000。那么使用旧的容量计算就是0000 … 0000 0000 1111和hash进行与运算。而新的容量就是 0000 … 0001 1111和hash进行与运算。我们发现两者的差别只有第五个位,两种情况1和0。那么这时候我们就可以利用这一点区分开来了。原来的hash值比如是0000 … 0000 1101 1111,和15进行与的时候,第五个1是不起作用的,因为15只有最后四个0,现在可以区分了。对于hash值0000 … 0000 1101 1111和 0000 … 0000 1100 1111,原来和15与运算结果都一样,现在和31运算,第五位为1的下标和第五位为0的下标刚好相差了一个旧容量的大小。图解如下:
在这里插入图片描述


//创建table或者扩容
//为什么要扩容:为了解决哈希冲突导致的链化影响查询效率的问题,扩容会缓解问题
final Node<K,V>[] resize() {
	 //当前oldTab 是 当前table
     Node<K,V>[] oldTab = table;
     //表示扩容之前的table数组的长度,如果是null,就创建,不是就扩容
     int oldCap = (oldTab == null) ? 0 : oldTab.length;
     //旧的阈值
     int oldThr = threshold;
     //新的容量和阈值
     int newCap, newThr = 0;
     //如果旧的容量大于0,说明此时的散列表已经初始化一次了,这是正常的扩容情况
     if (oldCap > 0) {
     	 //如果旧的容量大于最大的容量值
         if (oldCap >= MAXIMUM_CAPACITY) {
         	 //就把扩容阈值设置成Integer的最大值
             threshold = Integer.MAX_VALUE;
             //返回旧的table
             return oldTab;
         }
         //否则新的容量等于旧的容量扩展一倍,如果新的容量小于最大的容量以及旧的容量大于等于16的时候,
         //才可以对新的阈值赋值为旧的阈值*2
         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                  oldCap >= DEFAULT_INITIAL_CAPACITY)
             newThr = oldThr << 1; // double threshold
     }
     //oldCap = 0, oldThr不等于0,else if和else都是初始化
     //容量为0但是阈值大于0,下面三种情况都是只赋值了阈值没有初始化容量,也就是table还是空的
     //1. new HashMap(initCap, loadFactory)
     //2. new HashMap(initCap)
     //3. new HashMap(map) 并且这个map有数据
     else if (oldThr > 0) 
     	 //新的容量就用旧的阈值进行代替,因为旧的阈值一定是2的n次方倍
         newCap = oldThr;
     //旧的容量为0,旧的阈值也为0,是下面的初始化情况
     //new HashMap()
     else {
         //这时候容量默认为16,阈值默认为16*0.75 = 12              
         newCap = DEFAULT_INITIAL_CAPACITY;
         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
     }
     //如果新的阈值为0,是下面的四种情况
     //1. new HashMap(initCap, loadFactory)
     //2. new HashMap(initCap)
     //3. new HashMap(map) 并且这个map有数据
     //4. 旧的容量 < 16
     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"})
     //新的Table
     Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
     //新的table
     table = newTab;
     //如果旧的table不为空,需要对旧的table里面的结点分配空间
     if (oldTab != null) {
     	 //遍历旧的table
         for (int j = 0; j < oldCap; ++j) {
             Node<K,V> e;
             //判断数组的每一个下标的存储结构
             if ((e = oldTab[j]) != null) {
             	 //对旧的赋值,等待java自动垃圾回收
                 oldTab[j] = null;
                 //e的下一个是空,就证明了该下标下只有一个结点,比如table[0]下面只有一个结点
                 if (e.next == null)
                 	 //直接存入通过公式找到的新的table的新的下标
                     newTab[e.hash & (newCap - 1)] = e;
                 //如果e是树结点了,调用树的重新分配方法
                 else if (e instanceof TreeNode)
                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                 else {
                     //对于旧的table,扩容后,同一下标的结点只有2种可能,要么留在原来的下标,要么去到新下标,新下标为旧下标+旧容量
                     //没树化,也不是单个,证明是链表了
                     //低位链表:存放在扩容之后的数组的下标和原来的数组的下标位置一样
                     //高位链表:存放在扩容之后的数组的下标是原来的下标+旧的容量大小
                     //低位头和低位的尾部
                     Node<K,V> loHead = null, loTail = null;
                     //高位头部和高位尾部
                     Node<K,V> hiHead = null, hiTail = null;
                     //下一个结点
                     Node<K,V> next;
                     do {
                         next = e.next;
                         //这个就是上面的图解了,看懂了上面的这里就很好懂了
                         //结合上面的图,这里证明下一位是0,应该存放在低位
                         if ((e.hash & oldCap) == 0) {
                         	 //如果低位尾部null,证明低位还没放东西,存入头部
                             if (loTail == null)
                                 loHead = e;
                             else
                                 loTail.next = e;
                             loTail = e;
                         }
                         //这里证明下一位是1,应该存放在高位
                         else {
                             //如果高位尾部null,证明低位还没放东西,存入头部
                             if (hiTail == null)
                                 hiHead = e;
                             else
                                 hiTail.next = e;
                             hiTail = e;
                         }
                     } while ((e = next) != null);
                     //我们在低位头和尾部和高位的头和尾存放了数据了,自然要赋值到相应的下标
                     if (loTail != null) {
                         //赋值给低位下标
                         loTail.next = null;
                         newTab[j] = loHead;
                     }
                     if (hiTail != null) {
                         //赋值给高位下标
                         hiTail.next = null;
                         newTab[j + oldCap] = hiHead;
                     }
                 }
             }
         }
     }
     //返回新的table
     return newTab;
 }


5. remove

作用::移除并返回移除的value


 //两种remove,区别就是单纯根据key判断还是key和value同时判断
 public V remove(Object key) {
  Node<K,V> e;
     return (e = removeNode(hash(key), key, null, false, true)) == null ?
         null : e.value;
 }

@Override
public boolean remove(Object key, Object value) {
     return removeNode(hash(key), key, value, true, true) != null;
 }

    /**
     * @param hash hash值
     * @param key 键xbxt
     * @param value 值
     * @param matchValue 如果这个是true,那么要根据key和value来同时判断
     * @param movable 如果是false就不再去除其他结点了
     */
    final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
      /**
			tab : table
			p   : table某个下标的头结点
			n   : table长度,也是容量大小
			index : hash值求出的下标

	  */
      Node<K,V>[] tab; Node<K,V> p; int n, index;
      //table初始化了并且通过计算公式计算出的下标也是有数据的
      if ((tab = table) != null && (n = tab.length) > 0 &&
          (p = tab[index = (n - 1) & hash]) != null) {
          //node:找到了,就存储找到的结点,没找到就存储null
          Node<K,V> node = null, e; K k; V v;
          //对头节点进行判断
          if (p.hash == hash &&
              ((k = p.key) == key || (key != null && key.equals(k))))
              node = p;
          //如果头节点不是,就对下一个结点判断
          else if ((e = p.next) != null) {
              //不是空,就证明了要么是树,要么是链表
              if (p instanceof TreeNode)
                  //如果是红黑树,就要调用树的查找方法
                  node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
              else {
                  //不是树又不止一个结点,那就是链表了
                  do {
                      //一直不断循环该链表
                      if (e.hash == hash &&
                          ((k = e.key) == key ||
                           (key != null && key.equals(k)))) {
                          //如果找到了就赋值给node
                          node = e;
                          //直接退出
                          break;
                      }
                      p = e;
                  } while ((e = e.next) != null);
              }
          }
          //node !=null,证明找到了,如果matchValue 是true,就要进行后面的对value的判断;如果是false,就可以直接remove
          //达到了根据matchValue来选择是根据key还是根据key和value来同时判断
          if (node != null && (!matchValue || (v = node.value) == value ||
                               (value != null && value.equals(v)))) {
              //如果是树结点,就调用树的删除方法
              if (node instanceof TreeNode)
                  ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
              else if (node == p)
                  //否则如果node是头节点,直接把table[index]执行头节点的下一个就行了
                  tab[index] = node.next;
              else
                 //否则的话,把node的下一个赋给p的下一个,经过上面的循环,p->next=node,由于node一定是找到了
                 //不存在说node是null的
                  p.next = node.next;
              //修改次数+1
              ++modCount;
              //key-value键值对数量-1
              --size;
              //给LinkedHashMap进行remove的后置处理的
              afterNodeRemoval(node);
              //返回删除掉的结点
              return node;
          }
      }
      //空结没找到就返回空
      return null;
  }


5. clear

public void clear() {
    //table
    Node<K,V>[] tab;
      //修改次数,全部清空了也算一次修改
      modCount++;
      //table不为空
      if ((tab = table) != null && size > 0) {
      	  //设置size为0
          size = 0;
          //循环赋值为null,等待java进行垃圾回收
          for (int i = 0; i < tab.length; ++i)
              tab[i] = null;
      }
  }
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值