HashMap源码解析(含红黑树)

简介:

前两篇文章,我们分析了数组和链表,我们看到了数组和链表都有相应的缺点。有没有集两家之长的呢?有,那就是HashMap。

说到HashMap,不得不说一下hash,也叫散列。就是把任意长度的值,通过散列算法,得到一个固定长度的值。常见的几种Hash函数有直接寻址法、平方取中法、 除留余数法…
在这里插入图片描述
我们简单看下String的hashCode():
在这里插入图片描述
比较简单,遍历字符串,取每个字符串的ASCII值 * 31的n次幂,至于为啥是31,就不解释了,网上有专业的解释,我也没看 懂,反正只要知道性能杠杆的就行了。

HashMap简单来说,就是用key的哈希值去计算出在数组下标的位置,不考虑hash冲突的情况,只需要一次即可定位。当然冲突避免不了,而HashMap采用的是链地址法解决的,冲突了之后往后挂。本篇文章是基于jdk1.8。

UML图:
在这里插入图片描述
属性:

    //序列号ID
    private static final long serialVersionUID = 362498820763181265L;
    //默认的容量,也就是16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //负载因子,数组的长度 * 0.75,得到的就是临界值
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //阈值,超过这个阈值就会转成Tree结构
    static final int TREEIFY_THRESHOLD = 8;
    //阈值,低于这个阈值就会转成链表结构
    static final int UNTREEIFY_THRESHOLD = 6;
    //总元素的上限,超过这个上限,也会转成Tree结构
    static final int MIN_TREEIFY_CAPACITY = 64;
    //存储元素的数组
    transient Node<K,V>[] table;
    //另一种存储结构,遍历用的。
    transient Set<Map.Entry<K,V>> entrySet;
    //元素总个数
    transient int size;
    //修改次数
    transient int modCount;
    //临界值,一般等于table的长度的0.75
    //如果初始化的时候指定了长度,指定的长度就是临界值,不用 * 0.75。
    int threshold;
    //负载因子,默认是0.75,数组长度 * 负载因子等于临界值。
    final float loadFactor;

数据结构:
在这里插入图片描述
大概就是这个样子,数组的下标的节点有的是链表,有的是红黑树,具体看数量。

构造方法:
在这里插入图片描述
方法1很简单,方法2调的方法3,我们就先看下方法3。

方法3也很简单,就一个方法tableSizeFor(initialCapacity),计算传进来的初始长度是否的2的n次方,不是就改成是,我们来看下。
在这里插入图片描述
可以看到,经过一系列的计算,至于为什么这么计算我们就不深究了,但是最后的结果肯定是2的n次方,至于原因,下面会说。

再看下方法4,主要是putMapEntries(m, false),来看下:

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    
    
        //先得到m的元素数量,这里以HashMap为例
        int s = m.size();
        if (s > 0) {
    
    
            if (table == null) {
    
    
                //算出负载因子
                float ft = ((float)s / loadFactor) + 1.0F;
                //判断负载因子是否大于最大容量
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                        (int)ft : MAXIMUM_CAPACITY);
                //初始化的时候调用这个方法,threshold肯定为0,必定满足条件
                //相当于以丢进来的HashMap的元素个数为初始化长度
                if (t > threshold)
                    //确定下临界值
                    threshold = tableSizeFor(t);
            }
            //判断元素个数是否大于临界值
            else if (s > threshold)
                //扩容
                resize();
            //遍历元素,entrySet()后面说
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
    
    
                K key = e.getKey();
                V value = e.getValue();
                //循环put进map
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

其实很简单,就是以丢进来的HashMap的元素个数为初始化长度,然后遍历元素,重新put。

下面看一下HashMap的方法,这里只分析常用的方法。

put(K key, V value):

putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict):

由于put里面包含的东西太多,下面我们把put方法拆开来说。

大致流程就是:

1:判断数组是否为null:

//tab相当于上面的table
//p是要插入的下标的头节点
//n是数组的长度
//i是计算出来要插入的下标
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
            //扩容
            n = (tab = resize()).length;

2:计算下标:

//用hash计算下标,判断当前下标是否为null
if ((p = tab[i = (n - 1) & hash]) == null)

3:判断当前下标是否为null:

//为null就直接new一个新节点
tab[i] = newNode(hash, key, value, null);

4:判断是否是重复的key:

Node<K,V> e; K k;
//判断hash是否相等,判断key是否相等
if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
  //都相等,说明是重复key,记住这个节点
  e = p;

5:红黑树的新增:

else if (p instanceof TreeNode)
      //新增节点,新增成功会返回null。
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

6:链表的新增:

for (int binCount = 0; ; ++binCount) {
    
    
    //判断当前节点是否有下一个
    if ((e = p.next) == null) {
    
    
      //没有下一个就直接new一个新节点,把p的next指向这个新节点
      p.next = newNode(hash, key, value, null);
      //判断下节点数量是否大于阈值
      if (binCount >= TREEIFY_THRESHOLD - 1)
        //大于就要将链表转红黑树结构
        treeifyBin(tab, hash);
      break;
    }
  //判断hash是否相等,判断key是否相等
  if (e.hash == hash &&
      ((k = e.key) == key || (key != null && key.equals(k))))
    //都相等就说明找到一样的key了,跳出循环
    break;
  //e是当前节点的next,相当于下一个,此时的p就是下个节点,然后回上去继续判断。
  p = e;
}

7:判断是否需要替旧元素:

//判断e是否为null,e为null,就是添加成功,不为null就是不成功。
if (e != null) {
    
    
    //不为null就是有重复key,取出旧值
    V oldValue = e.value;
    //onlyIfAbsent是判断是否覆盖,true就是不覆盖,false就是覆盖。
    if (!onlyIfAbsent || oldValue == null)
      //替换掉旧值
      e.value = value;
    //为LinkedHashMap服务的
    afterNodeAccess(e);
    return oldValue;
}

上面大概注释了步骤,有些步骤详细说一说:

流程1里面的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;
    //判断老长度是否大于0,
  	//初始化的时候如果指定容量,仅仅指定了threshold,数组的长度还是0。
    if (oldCap > 0) {
    
    
        //老长度大于0,那么这就是真正的扩容,所以判断下是否大于最大容量
        if (oldCap >= MAXIMUM_CAPACITY) {
    
    
            //那么临界值也是最大容量
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //oldCap << 1相当于*2,如果*2之后小于最大容量并且大于等于初始化长度
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
            //新临界值就是老临界值的2倍
            newThr = oldThr << 1;
    }
    //判断老的临界值是否大于0
    else if (oldThr > 0)
        //如果老数组的长度是0,但是老临界值大于0,
      	//就说明初始化的时候指定了容量,这时候把初始化的容量赋给新数组的长度。
        newCap = oldThr;
    else {
    
    
        //如果老数组的长度、老临界值都是0,就说明初始化的时候并没有给定容量
        //那么新长度就是默认值,也就是16
        newCap = DEFAULT_INITIAL_CAPACITY;
        //新的临界值也是16*0.75
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //再次判断下新临界值是否等于0
    if (newThr == 0) {
    
    
        //新临界值等于0,只有一种可能,那就是初始化的时候指定了容量,重新算下临界值。
        //额,兜了一圈,临界值还是 * 0.75。
        float ft = (float)newCap * loadFactor;
        //把ft赋给新临界值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE);
    }
    //把新临界值赋给threshold
    threshold = newThr;
    //new一个新长度的数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //把新数组赋给table
    table = newTab;

总结一下:

如果是老数组有值,那么新长度 = 老长度 * 2,新临界值 = 老临界值 * 2

如果是初始化指定了长度,新长度 = 给定的长度,新临界值 = 给定的长度 * 0.75

如果是初始化没有指定长度,新长度 = 默认长度16,新临界值 = 默认长度16 * 0.75

  • 只有一个节点的复制:
//判断老数组是否为null
if (oldTab != null) {
    
    
  //不为null就遍历数组
  for (int j = 0; j < oldCap; ++j) {
    
    
    Node<K,V> e;
    //判断每个下标是否为null
    if ((e = oldTab[j]) != null) {
    
    
      //不为null的话就先把老数组当前下标置为null,方便GC。
      oldTab[j] = null;
      //判断此下标的节点的下一个是否为null
      if (e.next == null)
        //为null就说明此下标只有一个元素,重新计算下标,赋给新数组。
        newTab[e.hash & (newCap - 1)] = e;

比较简单,重新计算下标,赋给新数组。

红黑树的复制:

else if (e instanceof TreeNode)
    //复制红黑树
    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

看下split(),拆开来说:

  • 得到所有不需要改变下标的节点:
//遍历当前红黑树
for (TreeNode<K,V> e = b, next; e != null; e = next) {
    
    
    //得到当前节点的下一个
    next = (TreeNode<K,V>)e.next;
    //把当前节点的next置为null
    e.next = null;
    //判断是否需要改变下标,具体前面说过了
    if ((e.hash & bit) == 0) {
    
    
        //判断上一个是否为null
        if ((e.prev = loTail) == null)
            //为null此时的节点就是根节点,把e赋给loHead
            loHead = e;
        else
            //loTail此时是当前节点的上一个把loTail的next指向当前节点
            loTail.next = e;
        //把tail改成指向当前节点,其实就是一个临时变量,记住当前节点。
        //然后下次遍历进来的时候,要把上个节点的next指向当前节点,提前保存一下。
        loTail = e;
        //不需要改变下标的次数自增
        ++lc;
    }

细说:**if ((e.hash & bit) == 0)**这个判断,判断每个节点是否需要改变下标,等于0就不用改变下标:

HashMap每次扩容都是 * 2,先看直接计算的过程:
在这里插入图片描述
通过上面的计算过程,终于知道HashMap为什么要根据(e.hash & oldCap) 是否等于0判断要不要改变下标了。

那么为什么新下标 = 原下标 + 原数组长度呢?
在这里插入图片描述
因为数组长度是2的n次方,* 2之后,hash对应的那位bit如果是1,hash & oldCap -1的结果就是第n位多了1,等于 + 原数组长度。

再说复制节点:

链表的复制其实跟LinkedList的addAll()差不多,上篇文章已经详细描述过了,这里再简单配个图:
在这里插入图片描述
大家有没有觉得奇怪?这个是复制红黑树啊,怎么遍历的方法貌似就是遍历链表的方法呢?

不错,这里的确是遍历的链表。其实,尽管该下标的数据结构是红黑树,但是每个节点其实还是维护了一个next的,就是下个节点。

原因嘛,我估计:

1:是因为扩容之后,每个节点的下标可能会改变,而如果改变的节点较多,频繁的删除节点,红黑树的自我修正的次数会很多,浪费时间。所以这里干脆再维护一个链表,需要改变下标的是一个链表,不需要的是一个链表,然后重新转成红黑树。

2:是遍历的时候,就是取的每个节点的next遍历的,每个节点的next就是put进去的顺序。不过转红黑树的话,会把头节点改成root的,下面会讲。

  • 得到所有需要改变下标的节点:
else {
    
    
    //跟上面的意思一样
    if ((e.prev = hiTail) == null)
      	hiHead = e;
    else
      	hiTail.next = e;
    hiTail = e;
    //需要改变下标的次数自增
    ++hc;
}
  • 根据数量判断是转成红黑树,还是转成链表:
//判断loHead是否为null,说明此下标有值并且不需要改变下标。
if (loHead != null) {
    
    
    if (lc <= UNTREEIFY_THRESHOLD)
        //转成链表
        tab[index] = loHead.untreeify(map);
    else {
    
    
        tab[index] = loHead;
        if (hiHead != null)
            //转成红黑树
            loHead.treeify(tab);
    }
}
//跟上面的意思一样
if (hiHead != null) {
    
    
    if (hc <= UNTREEIFY_THRESHOLD)
      	tab[index + bit] = hiHead.untreeify(map);
    else {
    
    
        tab[index + bit] = hiHead;
        if (loHead != null)
          	hiHead.treeify(tab);
    }
}
  • 先看untreeify(HashMap<K,V> map):
final Node<K,V> untreeify(HashMap<K,V> map) {
    
    
    Node<K,V> hd = null, tl = null;
    //遍历
    for (Node<K,V> q = this; q != null; q = q.next) {
    
    
        //把TreeNode转成Node
        Node<K,V> p = map.replacementNode(q, null);
        if (tl == null)
          	hd = p;
        else
          	tl.next = p;
        tl = p;
    }
    return hd;
}

比较简单,就是把TreeNode转成Node。

treeify()请看本人另一篇文章,因为篇幅较长,红黑树部分另写了一篇,地址:红黑树篇

流程2里面的计算下标:

if ((p = tab[i = (n - 1) & hash]) == null),这里解释一下为什么数组的长度是2的n次方。

1:当数组的长度是2的n次方时,(n - 1) & hash,会满足一个公式:(n - 1) & hash = hash % n,但是 & 比 % 计算快。

2:当数组的长度是2的n次方时,(n - 1)之后就变成0000011111这样的,尾端全是1。
在这里插入图片描述
可以看到结果取决于两个:一个是(n - 1)最后有x个1,一个是hash的后x位。

而数组的长度一般是不会超过2的16次方的,所以正常来说key.hashCode()的高16位进行&运算 是没啥用的。

我们假设数组的长度是默认长度16,16-1就是15,而hash是从1开始:
在这里插入图片描述
可以看到hash是什么结果就是什么,所以上面的(key.hashCode()) ^ (h >>> 16),就是为 了让hash的低16位更加均匀,减少冲突。只要hash足够均匀,那么计算出来的下标就是均匀的。

而数组的长度如果不是2的n次方,假设数组的长度是15,15-1就是14,而hash还是从1开始:
在这里插入图片描述
可以看到,结果很不均匀,并且有些值是永远得不到的。因为你前面有效位开始有0了,进行 &运算,0永远是0。

正常其实用%计算就行了,但是%计算慢,所以HashMap采用了&。

流程5的putTreeVal、流程6的treeifyBin在另一篇文章说。
get(Object key):

public V get(Object key) {
    
    
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }    

    final Node<K,V> getNode(int hash, Object key) {
    
    
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
    
    
            //如果该下标不为null
            if (first.hash == hash &&
                    ((k = first.key) == key || (key != null && key.equals(k))))
                //哈希值一样,并且key相等,就返回。
                return first;
            //上面的没有return,说明第一个节点不是要找的,开始往下找
            if ((e = first.next) != null) {
    
    
                //判断是不是红黑树
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //不是红黑树,就是链表
                do {
    
    
                    //判断每个节点的哈希值和key是否相等
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                    //遍历链表
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

除了红黑树的getTreeNode另一篇文章说,其他的也比较简单。

remove(Object key):

public V remove(Object key) {
    
    
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
  • 第一步,先根据key找到节点:

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;
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;
//如果哈希跟key都相等
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 = e;
break;
}
//p是node是上一个节点
p = e;
} while ((e = e.next) != null);
}
}

根据key去查找节点,getTreeNode在另一篇文章分析,找到了之后准备删除。

//如果找到那个节点,还要判断matchValue是true或false
//matchValue是false代表找到相同的key就可以删除
//是ture代表还要验证value是否一样,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)
    //是的话直接把下标指向头结点的下一个节点
    tab[index] = node.next;
    else
      //p是要删除的上个节点,把上个节点的next指向删除节点的next。
      //跳过这个节点,这个节点就没了,相当于删除了。
      p.next = node.next;
    //操作次数自增
    ++modCount;
    //数量自减
    --size;
    //为LinkedHashMap服务的
    afterNodeRemoval(node);
    return node;
}

removeTreeNode在另一篇文章分析,可以看到,真正删除之前还判断了一下matchValue这个参数,其实这就是remove(Object key)和remove(Object key, Object value)的区别。链表的删除其实也很简单,prev的next指向删除的节点的next,next的prev指向删除的节点的prev。

示意图:
在这里插入图片描述
remove(Object key, Object value)其实上面已经说过了,put、get、remove都说完了,接下来看看遍历。

for (Object key : map.keySet()) {
    
    

}
for (Map.Entry<String, String> entry : map.entrySet()){
    
    

}

大家用的比较多的应该是上面的2种遍历方式,大家应该知道,像上面的增强for的遍历,实际上相当于迭代器。

上面的代码可以转成这样的:

Set<Object> set = map.keySet();
Iterator<Object> iterator = set.iterator();
while (iterator.hasNext()){
    
    
  	Object next = iterator.next();
}
Set<Map.Entry<Object, Object>> entries = map.entrySet();
Iterator<Map.Entry<Object, Object>> iterator = entries.iterator();
while (iterator.hasNext()){
    
    
    Map.Entry<Object, Object> entry = iterator.next();
    Object key = entry.getKey();
    Object value = entry.getValue();
}

按照上面的迭代代码,我们先看keySet():
在这里插入图片描述
KeyIterator继承了HashIterator,继续看下HashIterator的代码:

abstract class HashIterator {
    
    
    Node<K,V> next;        // next entry to return
    Node<K,V> current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;             // current slot

    HashIterator() {
    
    
        //拿到modCount
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) {
    
    
            //从0下标开始遍历,直到不为null的下标
          	//把值赋给next,这是第一个节点
            do {
    
    } while (index < t.length && (next = t[index++]) == null);
        }
    }
    public final boolean hasNext() {
    
    
        //判断next不为null
        return next != null;
    }
    final Node<K,V> nextNode() {
    
    
        Node<K,V>[] t;
        Node<K,V> e = next;
        //这里可以看modCount的作用,如果此时的HashMap被别的线程修改了,这里会不相等,会报错。
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        //从第一个节点next开始取next.next,直到为null,就是把当前下标的节点从头往后取,直到为null
        if ((next = (current = e).next) == null && (t = table) != null) {
    
    
            //再从新的index往下遍历
            do {
    
    } while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }

很简单,就是先遍历下标,然后把 每个下标的所有结点全部取出来。
在这里插入图片描述
再看entrySet():
在这里插入图片描述
EntryIterator也是继承了HashIterator,只不过一个是nextNode().key,一个是nextNode()。所以建议使用entrySet,keySet都已经拿到node了,结果只返回了key,当然人家名字就叫keySet。

说到这里,终于把HashMap啃完了,太不容易了,下篇分析一下队列。

猜你喜欢

转载自blog.csdn.net/couguolede/article/details/105540045