万字解析HashMap核心方法!

一 成员变量

        核心成员变量

成员变量名类型作用说明
transient Node<K,V>[] tableNode<K,V>[]存储键值对的哈希桶数组,是 HashMap 的主要数据结构,首次使用时延迟初始化
transient Set<Map.Entry<K,V>> entrySetSet<Map.Entry<K,V>>缓存的 entrySet() 结果,用于 Map 的遍历或迭代
transient int sizeint当前 Map 中键值对的数量
transient int modCountint结构修改计数器,支持 fail-fast 机制,迭代时检测并发修改
int thresholdint扩容阈值,等于 capacity * loadFactor,决定何时触发扩容
final float loadFactorfloat加载因子,控制容量使用率,默认是 0.75f

        常量成员变量

常量名类型默认值说明
DEFAULT_INITIAL_CAPACITYint16默认初始容量,必须为 2 的幂
DEFAULT_LOAD_FACTORfloat0.75f默认加载因子
MAXIMUM_CAPACITYint1 << 30最大容量
TREEIFY_THRESHOLDint8桶中链表节点数量超过此值(且 table 长度足够)会转为红黑树
UNTREEIFY_THRESHOLDint6红黑树节点数量不足该值时,退化为链表
MIN_TREEIFY_CAPACITYint64树化所需的最小 table 容量(低于此值不树化)

二 hash()

        我们先从最最最最最最重要的hash(Object key)入手,源码如下:

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

        代码非常简短,但其非常之重要,我们通常将其称为扰动函数(hash spreading function

        众所周知,map的底层结构是数组 + 链表(+红黑树),因为hash值针对不同的key可能出现相同的hash,所以就必然会产生哈希冲突,扰动函数就是为了减轻哈希冲突出现的可能性的,我们直接分析代码。

        其使用了一个三元运算符,左边的我们就不看了,直接看右边的一坨

(h = key.hashCode()) ^ (h >>> 16)

        首先调用了对象的hashCode()函数,之后获取对应的哈希值,将其赋值给h: h = key.hashCode()

        之后将其进行无符号右移动操作,将h向右边移动16wei位:h >>> 16

        高位则用0补充,那么这个时候,由于经过hashCode()方法获取的hash值一定是32位的(int 的范围大小),所以其高16位一定是空的,那么此时高16位置跟低16位就能够进行一定的操作了,这里使用了与运算(当且仅当两个位都是1才是1,其余情况都是0),以实现高位参与哈希桶的索引计算,进一步的降低哈希冲突发生的概率。

        这时有的同学就要问了,hashCode()到底是啥?简单来说,hashCode()就是提供对象的“身份哈希码”

        其实,hashCode方法就是用来获取某个对象的原始哈希值,这个方法是从Object类当中继承下来的

    @IntrinsicCandidate
    public native int hashCode();

        在不对其进行重写的情况下,其是一个native方法,底层是通过JVM实现的,将对应对象的地址通过一定的方式转换为某种变值,所以说这种方式下,对象的hash值跟对象的实际地址的高低是有关的。通过对其重写,来适应于某些特定场景。

        再总结一下,通过使用hash函数而不是直接调用hashCode,hashMap可以降低哈希冲突产生的概率,从而提高效率。

三 get()

        get()方法的源码如下:

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

        首先设置局部变量节点e,之后调用getNode()方法,获取对应节点并且将其赋值给e,使用三元运算符进行空值判断

return (e = getNode(key)) == null ? null : e.value;

        进入到核心方法,getNode(Object key)当中去看,源码如下:

    final Node<K,V> getNode(Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & (hash = hash(key))]) != 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;
    }

        逐层刨析,我们一步一步看:

Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
   if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & (hash = hash(key))]) != null)
       ......  
        }
        return null;
    }

        首先,定义了几个局部变量,其中:

        tab记录map中的节点数组table,first记录第一个节点 ,e用来记录当前节点,n记录当前map的table的长度,hash用来记录节点的哈希值,k用来记录当前Key;

        之后,看第一个if当中的条件判断,先将table赋值给tab: tab = table  同时判断其是否为空。

        再将当前tab的长度记录到n当中:n = tab.length 要求哈希表的长度大于0

        调用hash函数,获取当前key的hash值,并且将其赋值给hash:hash = hash(key) 同时要求其不为null

        OK,准备工作都做完了,我们已经能够保证对应key的有效性,同时获取到了对应的hash值等。

        再进一步看

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

        第一个if很简单,就是看当前第一个元素是不是我们要找的对象,先比较hash值,再通过比较对象的地址或者是参数值来看是否一致,如果都满足,直接返还第一个元素:

        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))

        这个代码主要针对首次就找到对应key的情况

        第二个if当中,先将first的下一个节点的值赋给e,同时确保不为空:(e = first.next) != null

        之后先判断当前桶下的节点类型是不是   树   这是针对 JAVA1.8版本更新的,如果是树类型的,那么则采用getTreeNode()的方式获取对应节点: 

if (first instanceof TreeNode)  return ((TreeNode<K,V>)first).getTreeNode(hash, key);    

        在1.8版本之前,hashMap的底层数据结构是:数组 + 链表,为什么又新增了红黑树?什么情况下用红黑树?我们等到讲到put再细说。

        进入到do while循环当中,这个就简单了,其实就是将当前桶中的元素,按照链表从前到后一个一个遍历,如果找到了,那么直接返还。

四 put()

        源码如下:

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

        内部调用了putVal函数,其中包含五个参数, 来看一下官方解释:

     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.

        前三个就不解释了,看后两个

        onlyIfAbsent:如果是true,那么就不修改已经存在的值,也就是说,如果你两次放置了同样的KEY和VALUE,那么当前参数如果为true,就不会基于原本的key去修改了;反之则修改。hashMap当中默认为false,基于原本KEY修改

        evict:如果为false,表明当前数组table正处于创建模式中。这里的创建模式代指两种情况:

        其一,当前table还没有被分配,HashMap还没有分配table

        其二,当前的table正处于扩容中

        hashMap中默认为true,即数组没有处于创建模式,这时候可以正常进行插入等操作。

        来吧,参数了解完了,我们来看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;
    }

        令人头大的代码。。。。

        我们一步一步看,拆分一下:

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

        首先声明一些局部变量,之后进入到第一个if,同时将map的数组table赋值给tab,table的长度给n。如果为空,或者是对应的table表长度为0,就调用resize()方法,即重新初始化tab表,对其设置长度,一般是对tab进行首次初始化操作,这里涉及到一个关键方法   resize() 我们等下面细说

        第二个if,将对应桶赋值给p,如果其为空:(p = tab[i = (n - 1) & hash]) == null

        那么表明当前桶还是空的,我们直接添加即可:tab[i] = newNode(hash, key, value, null);

        进入到else当中,put的核心代码区域,我们来看看到底写了啥:

        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 {
                .......
            }
            ......
        }

         第一个if,判断当前桶的第一个节点p的hash值是否跟需求的一样,同时比较对应key是否一致,如果是,那么将当前节点p赋值给e,这种情况适用于桶中的第一个节点的key跟对象key一致

if (p.hash == hash &&  ((k = p.key) == key || (key != null && key.equals(k))))  e = p;

        else if,如果当前桶下节点的类型是TreeNode,则表明其已经被树化了,将对应的节点放在红黑树中,调用puyTreeVal(...)方法:

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

       这个循环代码其实比较简单,binCount用来记录当前桶遍历过程中所处的位置以及链表长度,

        第一个if,将当前节点p的下一个节点赋值给e,如果为空,表明当前链表已经遍历结束了,直到遍历结束也没有发现对应符合要求的hash,也就是表明并不存在对应hash值,我们直接创建新的节点,并且将其赋值给p:p.next = newNode(hash, key, value, null);

        树化

        其内部还有一个if,我们看到常数 TREEIFY_THRESHOLD 一眼就能看明白,奥!这是对应将链表树化的长度!如果当前链表的长度binCount超过了树化的长度,这个时候,内部就会调用treeifyBin,从而直接将对应的链表转化为红黑树只要链表长度超过8就转化为树,真的是这样吗?这对吗?先放一放,我们先来看看这个常数是多少:

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

        叽里咕噜说了一堆,简单说就是若当前数组table的存储长度大于等于8,那么就将list转化为tree,即树化

        别急,通过源码我们还能看到,还有几个跟 Tree 有关的常数:

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

        UNTREEIFY_THRESHOLD : 也就是反树化的常数,当当前Tree的长度小于等于6的时候,就会将其重新转化为list集合。

        思考:不对啊,树化的时候是8,反树化不应该是7吗?中间为什么要差一个?

        其实这是特意这样设计的,因为有的时候,部分桶可能会反复的删除或者增加同一个KEY,这个时候,如果其刚好处于7 - 8之间,那么就一直在树化跟反树化之间蹦迪了,树化是非常耗时的!为了避免这种情况的发生,特意中间留了一个空间,避免一直“蹦迪”

        MIN_TREEIFY_CAPACITY :最小的table长度,如果表没有超过这个长度,那么就不对list集合进行树化,即使对应桶下的list长度超过了TREEIFY_THRESHOLD ,也不树化!换言之,两者一定是联合使用的

        我们回收一下问题:如果当前链表的长度binCount超过了树化的长度,这个时候,内部就会调用treeifyBin,从而直接将对应的链表转化为红黑树只要链表长度超过8就转化为树,真的是这样吗?这对吗?

        树化条件解析

        在这里我们能看到,并不是这样,在JDK1.8版本中,控制链表转化为红黑树是有两个限制条件的

        1. 桶下链表长度超过  TREEIFY_THRESHOLD

        2. table数组长度超过 MIN_TREEIFY_CAPACITY

        这一点何以见得呢?记得上面说的吗,put方法中,如果当前链表的长度超过了树化的长度,就会进入到方法treefiyBin

                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }

        如果满足了第一个要求之后,进入到treeifyBin源码当中:

    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条件判断,就是要求对应的tab为空时,或者对应桶的链表长度小于MIN_TREEIFY_CAPACITY的时候,对其重新进行resize() 扩容操作,也就是说,只有大于了MIN_TREEIFY_CAPACITY,才能执行树化的操作!也就满足了以上两点树化的要求

        扯的有点远了,回归正题,我把代码放下来,大家不用往上面翻了:

                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,如果当前的hash值跟我们需求的一样,并且对应key的值也一致(各位大佬到这里应该都能轻易看懂了),就说明我们找到了一个符合要求的链表中间的值,直接结束:

if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))

        确定了对应key的位置之后,下一步就该放入对应的值了,代码如下:

            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

        如果对应的e不为空,也就是说我们是找到了跟我们的key匹配的位置,如果是一直没找到,这里的e是空的,直接放在链表末端

        之后再判断一下,如果对应的节点e的值为空,或者是 !onlyIfAbsent,这个是最开始put传输过来到putVal的参数,记得吗?默认为fasle,即允许覆盖旧的key的值,它的作用就体现到这里了

     afterNodeAccess()

        然后会执行一个 afterNodeAccess(e);方法,在HashMap当中,这是一个钩子函数(Hook),即hashMap内部并没有实现,而是在其子类LinkedHashMap当中实现了

    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

        这种钩子函数,其本质其实还是  多态 

        当实现类为子类LinkedHashMap的时候,会执行子类的方法,例如:

        HashMap<Object, Object> linkedHashMap = new LinkedHashMap<>();

        这是JAVA多态的典型体现,即编译时看父类,运行时看子类

        还差最后一步,put方法就结束了:

        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;

        fail-fast机制        

        首先对成员变量modCount做了自增,利用fail-fast机制用于迭代时检测并发修改,我们简单说下fail-fast机制:

        在 HashMap 中,fail-fast机制(快速失败)是一种用于防止在遍历过程中发生结构性修改(如添加或删除元素)导致不一致行为的机制。它主要通过一个字段 modCount 来实现。

        即当你在使用迭代器(如 Iterator)遍历 HashMap 时,如果在遍历过程中修改了结构(put/remove),就会触发 ConcurrentModificationException,这就是 fail-fast。

  • 每次结构性修改(如 putremove)都会让 modCount++

  • 迭代器初始化时会保存 expectedModCount = map.modCount

  • 每次迭代时对比 modCountexpectedModCount

    • 通过比较,如果不一致,就说明结构被修改了 → 抛出异常。

        之后看当前的长度是否大于等于扩容容量,大于则进行扩容,之后依旧是调用钩子函数:afterNodeInsertion,依旧是子类LinkedHashMap的,主要是用于对插入节点之后,再做一些事情,这里我们就不详细说明了。

        还有一个扩容机制,我们等下一篇详细说,希望大家能耐心看完,并且有所收获  ^ - ^

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值