带你阅读JDK1.8的HashMap源码(一)

1.写在前面

前面的博客的我们简单的介绍完了红黑树,主要就是为了今天看1.8的HashMap的代码准备的,因为1.8的HashMap的源码有一个树化的过程,所以我们先简单的谈了下红黑树。

2.从put方法开始谈起

废话不多说,直接上代码,具体的put方法源码如下:

public V put(K key, V value) {
  return putVal(hash(key), key, value, false, true);
}

这儿我们调用的putVal(hash(key), key, value, false, true);方法,这儿我们需要注意的是第四个和第五个参数。

  • 第四个参数onlyIfAbsent:如果为真,则不要更改现有值,就是如果为真的时候,插入相同的键的时候,如果值不一样的话,则不会修改原来的键的值。很明显我们的HashMap是改变原来的值,所以这儿的值是false
  • 第五个参数evict:这个参数在HashMap中没有使用,我们这儿可以不用纠结。这个参数主要是在LinkedHashMap中使用。与今天的博客没有太大的关系,所以我们这儿直接跳过。

这儿我们还需要简单的了解下hash(key)方法的源码,具体的如下:

static final int hash(Object key) {
  int h;
  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

如果键为null,直接返回0,所以HashMap可以插入键为null的值。

如果键不为null,这个时候值等于先将这个键的hashcode计算出来,然后将这个值右移16位,然后在异或。这儿我们也不用深究其中的含义。我们只知道是这样计算出来的即可。

上面的方法大概讲完了,这个时候我们继续看源码,这个时候我们需要查看putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)方法,具体的代码如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  
  if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
  else {
    Node<K,V> e; K k;
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
      e = p;
    
    else if (p instanceof TreeNode)
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    
    else {
      for (int binCount = 0; ; ++binCount) {
        
        if ((e = p.next) == null) {
          p.next = newNode(hash, key, value, null);
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
          break;
        }
        
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
          break;
        
        p = e;
      }
    }
    
    if (e != null) { // existing mapping for key
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)
        e.value = value;
      afterNodeAccess(e);
      return oldValue;
    }
  }
  ++modCount;
  if (++size > threshold)
    resize();
  
  afterNodeInsertion(evict);
  return null;
}

上面的方法比较长,可能比较难搞,这个时候我们只需要分成几种情况,这样就比较好理解,这儿我们已经将对应的分支分开了,只要讲几种情况就行了。

2.1创建HashMap

这种的情况,主要是HashMap没有创建,刚刚开始创建,于是会进入第一个分支,具体的代码的如下:

if ((tab = table) == null || (n = tab.length) == 0)
  n = (tab = resize()).length;

这儿我们看的resize方法,具体的代码如下:

final Node<K,V>[] resize() {
  Node<K,V>[] oldTab = table;
  int oldCap = (oldTab == null) ? 0 : oldTab.length;
  int oldThr = threshold;
  int newCap, newThr = 0;
  if (oldCap > 0) {
    if (oldCap >= MAXIMUM_CAPACITY) {
      threshold = Integer.MAX_VALUE;
      return oldTab;
    }
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
             oldCap >= DEFAULT_INITIAL_CAPACITY)
      newThr = oldThr << 1; // double threshold
  }
  else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;
  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;
  @SuppressWarnings({"rawtypes","unchecked"})
  Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  table = newTab;
  if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {
      Node<K,V> e;
      if ((e = oldTab[j]) != null) {
        oldTab[j] = null;
        if (e.next == null)
          newTab[e.hash & (newCap - 1)] = e;
        else if (e instanceof TreeNode)
          ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
        else { // preserve order
          Node<K,V> loHead = null, loTail = null;
          Node<K,V> hiHead = null, hiTail = null;
          Node<K,V> next;
          do {
            next = e.next;
            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);
          if (loTail != null) {
            loTail.next = null;
            newTab[j] = loHead;
          }
          if (hiTail != null) {
            hiTail.next = null;
            newTab[j + oldCap] = hiHead;
          }
        }
      }
    }
  }
  return newTab;
}

一看方法这么长,是不是想死的心都有,不过不用担心,因为这个方法有两个职责,所以这个方法比较长,这个方法主要用于创建HashMap同时还有一种职责扩容,我们由创建的HashMap进入这个方法的,所以很多的代码我们暂时不用看的,只需要看我们创建HashMap的代码就行,于是代码精简成如下的内容:

Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
/*if (oldCap > 0) {
  if (oldCap >= MAXIMUM_CAPACITY) {
    threshold = Integer.MAX_VALUE;
    return oldTab;
  }
  else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
           oldCap >= DEFAULT_INITIAL_CAPACITY)
    newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
  newCap = oldThr;*/
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;
return newTab;

为了提高代码的可读性,所以我们这儿将不调用的代码注释掉了。

这儿主要四个比较重要的变量,分别是oldCap扩容前HashMap的容量,oldThr扩容前HashMap的负载因子乘以容量,newCap扩容后HashMap的容量,newThr扩容后的HashMap的负载因子乘以容量。这儿这4个值都是为0,因为这儿的HashMap还没有创建。那么这个时候,我们就需要创建这个HashMap,这个时候我们需要知道几个常量,具体的如下:

  • DEFAULT_INITIAL_CAPACITY的值为16
  • DEFAULT_LOAD_FACTOR的值为0.75

于是将DEFAULT_INITIAL_CAPACITY的值赋值给newCap,然后将DEFAULT_INITIAL_CAPACITYDEFAULT_LOAD_FACTOR的乘积赋值给newThr然后创建对应的长度的Node的数组返回,同时将这个创建好的值赋值给全局的变量table。同时将newThr这个变量赋值给全局的变量threshold。至此整个创建就完成了。

2.2插入(没有hash冲突的情况)

回到我们的原来的方法,我们继续看下一个分支,具体的代码如下:

Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((p = tab[i = (n - 1) & hash]) == null)
  tab[i] = newNode(hash, key, value, null);

这儿我们需要的知道的这个数组的下标我们是怎么知道的,主要通过下面的式子计算出来的,具体的如下:

i = (n - 1) & hash

这儿的n是16然后减去1就是15然后与上hash的值,这个时候我们会得到一个下标,然后获取这个数组对应的下标的这个值,如果为空,就直接创建这个Node节点。这个时候有一个疑问,**我们都知道前面的方法创建的这个HashMap的Node的数组的长度就是16,那么我们怎么一定肯定计算出来的这个下标就一定是0到15之间的值呢?**这个问题稍后再回答。我们先看看 newNode(hash, key, value, null);方法,具体的代码如下:

tab[i] = 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);
}

需要注意的是这儿这个Node的这个对象的下一个节点为null,也是没有问题的,因为这儿就是为null,这个下标首次插入对应的值,它的下一个节点就是为空。

回到刚才的问题,我们都知道前面的方法创建的这个HashMap的Node的数组的长度就是16,那么我们怎么一定肯定计算出来的这个下标就一定是0到15之间的值呢?

我们需要再次看一下计算hash的方法,具体的代码如下:

int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
h >>> 16
h       0000 0000 0000 0001 0000 0000 0000 0000
h >>>16 0000 0000 0000 0000 0000 0000 0000 0001

这儿右移16位的话,就是int的值的高16位移动低16位,我们再来看下异或

(h = key.hashCode()) ^ (h >>> 16)
h       0000 0000 0000 0001 1000 0000 0000 0000
h >>>16 0000 0000 0000 0000 0000 0000 0000 0001
^       0000 0000 0000 0001 1000 0000 0000 0001

也就是hashcode的值的高16位于16位异或这个值作为结果值的低16位,然后原来的hashcode的值的高16位不变作为结果值的高16位

这个时候我们再来看下标的计算方法

i = (n - 1) & hash
15   0000 0000 0000 0000 0000 0000 0000 1111
hash 0000 0000 0000 0001 1000 0000 0000 0001
&    0000 0000 0000 0000 0000 0000 0000 0001

由于是与运算,所以这儿不管是什么值,与上15一定是0到15之间的值。

2.3插入(有hash冲突的情况)

有hash冲突的情况下,又有两种情况,一种是链表的插入,一种是红黑树的插入。我们先看链表的插入

2.3.1链表的插入
Node<K,V> e; K k;
if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
  e = p;
/*else if (p instanceof TreeNode)
  e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);*/
else {
  for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == null) {
      p.next = newNode(hash, key, value, null);
      if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
        treeifyBin(tab, hash);
      break;
    }
    if (e.hash == hash &&
        ((k = e.key) == key || (key != null && key.equals(k))))
      break;
    p = e;
  }
}
if (e != null) { // existing mapping for key
  V oldValue = e.value;
  if (!onlyIfAbsent || oldValue == null)
    e.value = value;
  afterNodeAccess(e);
  return oldValue;
}

为了代码的可读性,我又注释了不用的代码,然后我们看对应代码。

这儿也是两种情况,我们先看插入的key是一样的

  • 这儿的p是我们查找出来的Node,如果这两个Nodehash的值是一样的,同时这两个Node的值key是一样的,那么我就用e接受这个查找出来的Node
  • 然后根据onlyIfAbsent的值,如果这个值为false,我们就修改相同key的值,同时将这个key的原来的value的值返回出去。

第二种情况,就是插入key的是不一样的

  • 这个时候我们直接进入else的的分支中去。先遍历这个找到的节点对应链表的尾部,然后直接插入进去。这个时候我们需要知道常量

    TREEIFY_THRESHOLD的值为8,也是说当链表的长度大于8的时候,我们需要转成红黑树。

  • 这个时候在遍历的时候可能找到对应的key是一样的,这个时候直接结束循环,然后走剩下的流程,就是和第一种的情况差不多。

这个时候我们需要看看这儿怎么将链表转换成红黑树的方法treeifyBin(tab, hash);,具体的代码如下:

final void treeifyBin(Node<K,V>[] tab, int hash) {
  int n, index; Node<K,V> e;
  if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    resize();
  else if ((e = tab[index = (n - 1) & hash]) != null) {
    TreeNode<K,V> hd = null, tl = null;
    do {
      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);
    if ((tab[index] = hd) != null)
      hd.treeify(tab);
  }
}

这个时候我们进来第一个if的分支,然后我们发现数组的长度是小于**MIN_TREEIFY_CAPACITY(64)**这个时候我们继续看resize()方法。具体的代码如下:

final Node<K,V>[] resize() {
  Node<K,V>[] oldTab = table;
  int oldCap = (oldTab == null) ? 0 : oldTab.length;
  int oldThr = threshold;
  int newCap, newThr = 0;
  if (oldCap > 0) {
    /*if (oldCap >= MAXIMUM_CAPACITY) {
      threshold = Integer.MAX_VALUE;
      return oldTab;
    }*/
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
             oldCap >= DEFAULT_INITIAL_CAPACITY)
      newThr = oldThr << 1; // double threshold
  }
  /*else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;
  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;
  @SuppressWarnings({"rawtypes","unchecked"})
  Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  table = newTab;
  
  return newTab;
}

扩容的代码我已经将这次流程不走的代码注释掉了,只看主要的流程。首先还是这四个变量。不懂的可以看看前面的介绍这4个变量,这儿不做过多的赘述。首先进入的是第一个if的判断,newCap变成原来的oldCap的两倍,前提有一个条件就是newCap小于MAXIMUM_CAPACITY(int的最大的值)同时oldCap大于DEFAULT_INITIAL_CAPACITY(16)这个时候newThrnewCap变成了原来的两倍了。

然后就是一些赋值的操作,这个时候我们需要继续看剩下替换的功能。具体的代码如下:

if (oldTab != null) {
  for (int j = 0; j < oldCap; ++j) {
    Node<K,V> e;
    if ((e = oldTab[j]) != null) {
      oldTab[j] = null;
      if (e.next == null)
        newTab[e.hash & (newCap - 1)] = e;
      else if (e instanceof TreeNode)
        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
      else { // preserve order
        Node<K,V> loHead = null, loTail = null;
        Node<K,V> hiHead = null, hiTail = null;
        Node<K,V> next;
        do {
          next = e.next;
          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);
        if (loTail != null) {
          loTail.next = null;
          newTab[j] = loHead;
        }
        if (hiTail != null) {
          hiTail.next = null;
          newTab[j + oldCap] = hiHead;
        }
      }
    }
  }
}

上面的代码主要是重新分配下HashMap的值。主要分成三种情况

  • 第一种情况:就是这个数组的下标的值就只有一个值,这个时候只需要重新计算一下下标就可以了。然后放到新的数组中去。
  • 第二种情况:就是这个遍历出来的元素,是一个树节点,就直接调用((TreeNode<K,V>)e).split(this, newTab, j, oldCap);方法。
  • 第三种情况:就是这个遍历的出来的原始,是一个链表,遍历这个链表,将这个链表分成两个链表,主要是这个键的hash的值与上这个原来的数组的长度,如果为0的话,就放到lo的链表中,如果不为0的话,就放到hi的链表中去。计算完了,将lo的链表挂到新的HashMap中去,下标是当前遍历的下标中去。将hi的链表挂到新的HashMap中去,下标是当前遍历的下标加上原来HashMap长度的下标中去。

最后再回到原来的树化的流程,具体的代码如下:

final void treeifyBin(Node<K,V>[] tab, int hash) {
  int n, index; Node<K,V> e;
  /*if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    resize();*/
  else if ((e = tab[index = (n - 1) & hash]) != null) {
    TreeNode<K,V> hd = null, tl = null;
    do {
      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);
    if ((tab[index] = hd) != null)
      hd.treeify(tab);
  }
}

先将这个节点转换成TreeNode节点,然后遍历这个链表,转换成双像链表,同时将这个链表转换成功红黑树,主要调用的方法是hd.treeify(tab);

2.32红黑树的插入
Node<K,V> e; K k;
/*if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
  e = p;*/
else if (p instanceof TreeNode)
  e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
/*else {
  for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == null) {
      p.next = newNode(hash, key, value, null);
      if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
        treeifyBin(tab, hash);
      break;
    }
    if (e.hash == hash &&
        ((k = e.key) == key || (key != null && key.equals(k))))
      break;
    p = e;
  }
}
if (e != null) { // existing mapping for key
  V oldValue = e.value;
  if (!onlyIfAbsent || oldValue == null)
    e.value = value;
  afterNodeAccess(e);
  return oldValue;
}*/

主要调用的是 ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);方法,由于篇幅的问题,有关红黑树的操作,我们下篇博客我们继续讲。

3.总结

具体的流程图如下:

在这里插入图片描述

4.写在最后

这篇博客主要介绍了1.8JDK的HashMap的方法,大概的介绍了流程,有关红黑树的操作,下篇博客我们继续介绍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值