容器之HashMap1.8源码解析

前言

这篇文章会对HashMap的源码进行解析,HashMap不光是我们日常开发会经常用到,在面试时也经常会被问到。希望大家看完这篇文章之后对于HashMap不光知其然,也能知其所以然。


一、put(K key, V value)

大家在用到HashMap时,都知道要put方法需要两个参数,一个key,一个value。那么具体是怎么进行存储的呢,接下来我们就先看看put方法的源码。

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

首先我们不管putVal这个方法是做什么的,先看下hash(key)这个方法是什么意思,返回了什么值,先搞清所有的参数,再去看putVal。

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

可以看出这个方法返回的是一个int值,(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)的意思,大家都是老司机了,应该也不陌生,如果传入的key是null,那么就返回0,否则返回(h = key.hashCode()) ^ (h >>> 16)。重点就是这里,可以称之为扰动函数。实际就是h和h >>> 16进行按位异或运算(两个数转为二进制,然后从高位开始比较,如果相同则为0,不相同则为1)。既然是右移16位,那么我们可以以16作为一个分水岭。如果本身大于16位(高16位中有1),那么右移16位之后,低16位全部消失,就变成了高16位全是0,低16位有1。换句话说就是高16位不变,低16位与高16位进行异或运算,增加低位的随机性。如果本身就小于等于16位,那么其实就没有影响,原样输出,不会触发扰动函数。

接下来回到putVal,这里我们先不赘述,我们先来了解一下resize()方法,扩容方法也是HashMap中最重要的方法。

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

首先定义oldTab并将table赋值给它,这个table是本身定义的一个空的Node数组,所以在我们第一次put时,这个oldTab和table都是null。oldCap 就是旧的容量,如果oldTab是null,那么它就是0,否则就是现有数组的长度。oldThr 赋值阈值。接下来赋值新容量和新阈值为0。resize方法太长了,我们分解来看。

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;

判断语句:
1.如果旧容量大于0,也就是说本身已经存在数据了。再次判断:
(1)如果老table数据长度大于等于MAXIMUM_CAPACITY(1<< 30),那么阈值就等于Integer.MAX_VALUE。直接返回老table。之前总提到阈值,其实阈值的意思就是,每一次扩容不是说要等到容量全部都满了才去扩容,而是到达一定数量就开始扩容,这个数量就是阈值 。上面也提到过阈值=容量*负载因子。eg.现在容量是16,负载因子是0.75,那么阈值就是12,也就是当数组长度达到12的时候就开始扩容了。
(2)newCap = oldCap << 1就是现在新容量是旧容量的2倍,DEFAULT_INITIAL_CAPACITY(1 << 4)=16。这样就能明白,如果新容量小于MAXIMUM_CAPACITY并且旧容量大于16,那么新的阈值赋值为旧阈值的2倍。
2.如果旧表没数据并且旧阈值大于0,新容量就等于旧阈值。
3.如果上面两种情况都不满足,即旧表没数据,并且没有初始化容量和阈值。那么新容量就为默认容量16,新阈值就是默认容量16乘以默认负载因子0.75等于12。

接下来判断newThr == 0,即对应的是当前表是空的,但是有阈值的情况。如果等于0就给newThr进行赋值,然后进行越界修复。
最后将新阈值赋值给threshold这个全局变量。

这部分代码其实就是根据情况,计算出新的容量和新的阈值。

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

我们计算完新的容量和新的阈值,接下来这部分,其实想想都知道,肯定是要把旧table的内容放进新table里,我们来看看是怎么做的。
首先根据新容量定义一个新的Node数组。然后判断旧table是否为null,如果旧table有数据,那么就正式开始操作了。大家都知道HashMap是数组+链表结构,for循环中的j其实就是数组的index。如果oldTab[j]不是null,那么先将oldTab[j]指向null,通知回收。然后开始条件判断:
1.如果e.next == null,也就说e是链表中的最后一个节点,已经没有后置节点了。那么将e.hash 和(newCap - 1)进行按位与操作,计算出新的index,然后将e放在新表的新下标的链表中。
2.在这种情况下,e就是一个树节点。也就是转到红黑树结构了,篇幅有限这里就不细讲了,大家可以看下其他的视频或文章学习下。
3.看似很复杂,其实就是分成了两个部分,高位区和低位区,高位区和低位区可以理解为,低位区就是原来的index区间,高位区是扩容后增加的index区间,比如旧容量是16,扩容后变成32,那么现在低位区就是index0-15,高位区就是index16-31。Head和Tail对应的就是头尾指针。逻辑就是将原来每个下标中的链表进行循环,之后并不是赋值在原来的位置上了,而是通过判断,将一部分迁移到高位区。如果对代码中头尾指针的移动有困惑可以看我前一篇讲LinkedList和ArrayList源码的文章,能有所帮助。之后就是将高位区和低位区进行结合组成newTable,然后返回。

到这扩容方法结束,我们现在转回去看一下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;
    }

定义好变量后,进行判断,(tab = table) == null || (n = tab.length) == 0,这个意思就是如果是一个全新的table,那么这个时候n是多少呢?上面扩容方法中讲过全新的table首次扩容,默认容量是16。所以这个时候n=16。
接下来是一个if else的判断:如果计算后的下标i在数组中没有数据,那么就新建一个节点,否则:
1.如果与已存在的Node是相同的key值,直接替换;
2.判断是否是树节点;
3.确定是链表的普通节点,循环遍历链式Node,并对比hash和key,如果都不相同,则将新的Node拼装到链表的末尾。如果相同,则进行更新。
接下来其实就是接着步骤1,满足条件就进行值替换。onlyIfAbsent 如果是true,那么就不改变已存在的value值。
接下来增加修改次数,如果现在的size大于阈值就进行扩容。

到这HashMap的put方法就结束了。可能来回跳的有点乱,建议大家重点看一下reSize方法,然后对照着源码整个过一遍,有问题的地方再看回到这里。

二、get(Object key)

get方法肯定传入一个key来获取value,那么这个value是怎么来的,我们接着来看源码。

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

首先我们不管getNode方法具体做了什么,两个参数,一个是对key进行hash,另外一个就是key。返回的肯定是个Node,如果是个null,那么就直接返回null,否则就返回这个节点的value。接下来我们就看看getNode到底是怎么get的这个Node。

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

首先就是判断table不是null并且tab[(n - 1) & hash]不是null,那么就再进行判断,否则直接返回null。
接下来判断的就是当前下表链表的第一个结点就符合条件,那么就直接把第一个节点返回。
如果first节点的下一个节点不是null,那么先判断是不是树节点,如果是树节点,就从红黑树取值,如果不是就循环遍历链表进行取值。
如果最后还是没有符合条件的就返回null。

总结

上面主要讲了HashMap的put和get的源码。接下来再补充一下。
1.转红黑树的条件不止是链表长度达到了转树的阈值(默认为8),在树化(treeifyBin)方法中还进行了判断,tab的长度要小于MIN_TREEIFY_CAPACITY(默认64)。当然红黑树还可以转回链表,条件是当链表的值小于UNTREEIFY_THRESHOLD(默认6)。为什么会是链表大于8就树化,这里转载一个链接给大家,可以参考为什么?侵删。
2.如果大家看过源码之后还是有点蒙,或者有点看的一知半解,这里建议大家debug,跟着流程走一下,可能会加深理解,并且建议key采用Integer类型,因为Integer类型hashCode的值就等于本身,方便后续自己的计算。
3.不管是put还是get都重点运用了hash和equals方法。这里的equals方法是重写过的因为equals方法本身只对引用进行比较。

因为我的了解可能也不是那么深刻,所以难免有错误或者没有提到的地方,有问题希望大家能在评论中指正,大家一起讨论,共同进步!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值