HashMap源码分析

HashMap底层数据结构

红黑树

我们都知道,jdk1.8以后 HashMap的底层是由数组+链表+红黑树组成的,那么什么是红黑树?

二叉搜索树(二叉查找树)

我们都知道二叉树,即每个节点最多有两个子节点。

一颗无序的二叉树,如果我们要找树中的某个节点应该怎么做?

遍历树的每个节点,直到找到我们需要的节点,这样效率为O(n),效率低下,有没有什么简单的方法?

于是二叉搜索树出现了,我们规定对树的任何一个节点而言,它的左子树的所有节点=<根节点=<右子树的所有节点

这样我们的查找效率就变为 O(logN)

AVL树

如果我们不限制二叉搜索树的高度,可能会出现下面这种情况:

所有的节点集中在一侧,此时查找效率低下,背离了我们建立二叉搜索树的初衷!

于是平衡树就应运而生了。AVL树就是一种平衡二叉搜索树。

它的主要思想是 对于任何一个节点,它的左子树跟右子树的高度差不超过1。

红黑树

红黑树是另一种平衡树,它的条件比较复杂:

  • 每个结点的颜色只能是红色或黑色。

  • 根结点是黑色的。

  • 每个叶子结点都带有两个空的黑色结点(null)。

  • 如果一个结点是红的,则它的两个儿子都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。

  • 对于每个结点来说,从该结点到其子孙叶结点的所有路径上包含相同数目的黑结点。

Entry,Node,TreeNode

Entry是一个接口,Node是Entry的实现类,存储了(key,value)键值对,TreeNode是Node的子类用在红黑树中

Entry -> Node -> TreeNode

HashMap源码

        HashMap map = new HashMap();
        map.put("java",10);
        map.put("php",10);
        map.put("java",10);

1.执行构造器HashMap map = new HashMap();时,进行了初始化 加载因子=0.75

此时我们的table (Node数组)为空

2.put元素时,会先计算key的hash值,hash值的计算是由key的hashCode经过一个扰动函数

3.执行putVal

    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;
    }

首先判断table是否为空,由于我们是第一次添加元素,此时table为空,因此会进入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;
    }

第一次扩容,table的长度为16! 临界值=0.75*16=12。当table中实际元素的数量大于这个临界值之后,会再次进行扩容,之后的扩容table的长度变为2倍,临界值变为2倍!

3.接着我们根据key的hash,把Node放到数组中对应的位置

为什么要用(n - 1) & hash计算对应数组下标呢?

(n-1)&hash其实一种高效的取余操作! 其等效于hash%n

4.如果该位置已经有Node节点了呢?

我们先判断key的hash值和已有节点的key的hash值是否相同,key是否equals已有节点的key(也就是判断究竟是不是同一个key)

如果是同一个key则替换掉节点的val

如同不是同一个key,则判断已有节点是不是TreeNode类型,如果是则说明该位置连接的是棵红黑树接下来往树中添加节点,如果不是则说明该位置连接的是链表,接下来往链表中添加节点

5.如何往链表中添加Node?

依次取出当前链表中的所有节点,判断key是否相同,如果相同则替换掉val。

如果不同则在链表的结尾添加上新的Node。

如果链表的Node个数>=8时,会进行树化!(转化成红黑树)

需要注意,只有在当前table的length>=64的时候才会真正发生树化! 否则扩容

6.如何往红黑树中添加Node?

7.如何树化

树化的条件: table.length>=64 并且 链表的长度>=8

8.get()方法:

get()方法没什么好说的,就是根据hash值经过扰动,取余后找到在table中对应的位置,取出Node节点一个个进行比较,找到对应的key后取出value即可

9.扩容

除去第一次添加元素所进行的扩容外,其余每次扩容容量都变为原来的2倍。

扩容后,需要重新计算table中每个Node的位置

比如原来length=16,扩容后为32。

length-1由 1111 --> 11111

多了一位1!

在计算hash &(length-1) 的时候,实际上只需要看多出来的这一位,hash值如果该位为1 则新的位置为 oldlength+原位置

举个例子假如hash值为10001,扩容前的位置为 01111(old-length-1) & 11001(hash)=01001。扩容后的位置为 11111(new-length-1) & 11001(hash)=11001 相当于 01001+10000(16的二进制)。

hashmap的扩容就是这样,将链表分为两条链表,一条放在原位置,一条放在新位置上,节点的相对顺序不变

树的退化

当树中节点数目<=6时 树会退化为链表

为什么需要扰动函数?

我们都知道hashcode是一个int类型的有32位,地址空间长度为40亿,肯定不能直接映射到数组的下标上

hashmap采用的是(length-1)&hash的方法(等效于取余),以初始容量length=16为例,(length-1)&hash只用到了 低4位的信息。

如果只用到低位的信息,那么hashcode碰撞的几率是很大的。

扰动函数的作用就是将高位的信息映射到低位中去。减少哈希碰撞的几率。

以jdk1.8的扰动函数为例,hash=hashcode的低16位 ^ 高16位。

同时考虑到边际效应,jdk1.8只扰动了一次。

(length-1)&hash

先上结论:假设被除数是x,对于除数是2n的取余操作x%2n,都可以写成x&(2^n-1),位运算效率高!

网上对这个原因的解释都是模糊不清,下面是我对于这个等式为什么成立的一些理解。

就拿这个 259%8 进行举例。


eg:259%8=259&7=3

259    100000011
7      000000111
259&7=011=3

259的二进制为100000011,8的二进制为1000。

假如对8进行取余,那么只需要留下最后4位,前面的都可以舍弃,为什么呢?

因为比8更高位的都来自于8的2次幂,所以高位的1都是可以整除8,可以直接舍弃。

这解释了为什么只有除数是2n才可以这样的操作,因为如果除数是9的话,高位不一定能整除9,无法舍弃所以不行。(其余2n除数也同理)

也解释了为什么位数不同也可以做&操作,因为比除数高的位都是可以舍弃的。

然后就是&操作了,直接&8的话是不行的,假设最后四位是1XXX,那么1XXX&1000=1000,很明显对8取余得到的结果不可能是8,余数应在0到除数减一之间,

所以我们需要对&7,让取余结果最大在0-7之间。

这就解释了为什么要&(2^n-1)。

那有些人会问了,那我就是要&8,不就是相当于对9取余吗?

对9取余就违反了上面的:“只有除数是2^n才可以这样的操作”。

树化的阈值为什么是8?

链表的平均查询次数为O(n/2) 红黑树平均查找次数为O(logn) 当n为8时 log8=3 ,8/2=4,红黑树优于链表

jdk的设计者认为,
如果 hashCode的分布离散良好的话,那么红黑树是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,注释中给我们展示了1-8长度的具体命中概率,当长度为8的时候,概率概率仅为0.00000006,这么小的概率,HashMap的红黑树转换几乎不会发生

树退化的阈值为什么是小于等于6,而不是小于8

留有中间地带,防止频繁的进行红黑树和链表的转化。

posted @ 2021-08-26 20:33  刚刚好。  阅读(33)  评论(0编辑  收藏  举报