HashMap源码分析(一)

作者: 键盘上的麒麟臂 | 来源:发表于2019-12-10 22:33 被阅读0次

    这次只讲解部分源码,不会全部讲完。并且代码来自API 26 Platfrom。前段时间又重新简单看了一次HashMap的源码,想简单记录一下碰到的问题和在源码中能参考到的代码写法。

    我先提出我的几个问题,如果有大佬路过的话麻烦请帮忙解答一下:
    为什么获取hash的时候要与自身的右移动16位做异或运算
    为什么根据key生成下标的做法是 (n - 1) & hash
    为什么扩展数组的时候拆分链表的条件是e.hash & oldCap

    接下来进入正题

    1.HashMap节点结构体

    可以先看看节点的数据结构

        static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;
            final K key;
            V value;
            Node<K,V> next;
    
            Node(int hash, K key, V value, Node<K,V> next) {
                this.hash = hash;
                this.key = key;
                this.value = value;
                this.next = next;
            }
    
            public final K getKey()        { return key; }
            public final V getValue()      { return value; }
            public final String toString() { return key + "=" + value; }
    
            public final int hashCode() {
                return Objects.hashCode(key) ^ Objects.hashCode(value);
            }
    
            public final V setValue(V newValue) {
                V oldValue = value;
                value = newValue;
                return oldValue;
            }
    
            public final boolean equals(Object o) {
                if (o == this)
                    return true;
                if (o instanceof Map.Entry) {
                    Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                    if (Objects.equals(key, e.getKey()) &&
                        Objects.equals(value, e.getValue()))
                        return true;
                }
                return false;
            }
        }
    

    用一个静态内部类来定义节点,节点里面有4个属性
    (1)hash:这个节点的hashcode
    (2)key/value:键值对
    (3)next:指向下一个节点的指针
    hashmap内部的操作基本都是对节点进行操作。

    2.重要参数

    hashmap中有几个重要的参数,在源码中也有明显的注释



    这样的注释可以让开发者更快的找到相应的功能模块,如果一个类里面代码量多的话我也会这么写注释。

    transient Node<K,V>[] table;
    transient Set<Map.Entry<K,V>> entrySet;
    transient int size;
    transient int modCount;
    int threshold;
    final float loadFactor;
    

    (1)table就是节点的数组,java中hashmap基本的形态就是一个链表数组(这是我的理解,不知道这样说对不对,反正就是数组和链表的结合),table就是这个数组。
    entrySet本章先不解释
    (2)size是长度,不是数组的长度,而是hashmap中包含的键值对的长度。
    (3)modCount是操作次数,这个本章也不会细讲。
    (4)threshold是数组的扩展容量(我的理解),往下看就知道有什么用了。
    (5)loadFactor加载因子,往下看就知道有什么用了。

    3.构造方法

    构造方法是一个类的入口,所以我们先从构造方法看。

        public HashMap(int initialCapacity, float loadFactor) {
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal initial capacity: " +
                                                   initialCapacity);
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new IllegalArgumentException("Illegal load factor: " +
                                                   loadFactor);
            this.loadFactor = loadFactor;
            this.threshold = tableSizeFor(initialCapacity);
        }
    
        public HashMap(int initialCapacity) {
            this(initialCapacity, DEFAULT_LOAD_FACTOR);
        }
    
        public HashMap() {
            this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
        }
    

    这里有3个构造方法,两个参数分别是initialCapacity表示数组容量,loadFactor表示加载因子,简单了解下就行,按照最常见的用法,我们一般都是调用无参的构造方法
    加载因子默认为DEFAULT_LOAD_FACTOR,也就是0.75(看下面的源码就知道loadFactor有什么用了)

    4.put方法

    我们一般调用无参构造函数实例化对象之后,就会调用put方法,也就是只设置了loadFactor的值之后就调put方法

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

    第一个参数为key的hashcode,我们可以看看hashcode怎么生成的

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

    看到有调用object的hashCode()方法。

        public int hashCode() {
            return identityHashCode(this);
        }
    
        // Android-changed: add a local helper for identityHashCode.
        // Package-private to be used by j.l.System. We do the implementation here
        // to avoid Object.hashCode doing a clinit check on j.l.System, and also
        // to avoid leaking shadow$_monitor_ outside of this class.
        /* package-private */ static int identityHashCode(Object obj) {
            int lockWord = obj.shadow$_monitor_;
            final int lockWordStateMask = 0xC0000000;  // Top 2 bits.
            final int lockWordStateHash = 0x80000000;  // Top 2 bits are value 2 (kStateHash).
            final int lockWordHashMask = 0x0FFFFFFF;  // Low 28 bits.
            if ((lockWord & lockWordStateMask) == lockWordStateHash) {
                return lockWord & lockWordHashMask;
            }
            return identityHashCodeNative(obj);
        }
    
        @FastNative
        private static native int identityHashCodeNative(Object obj);
    

    简单可以看出,这里是调用原生的方法生成的,那么我这里并没有追去看原生怎么生成的,我只是去网上收看到有人说生成的hashcode是内存地址,32位,如果不是这样的话希望能有大佬在评论指正
    那么这里获取hash值就是用内存地址异或内存地址右移16位,至于为什么这样做,我也不知道,这说明即便光看源码,也并非什么都能看懂,我查了下,听被人说大概的意思是这样做能减少碰撞的次数,这个我也没认证过。

    回头继续看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;
        }
    

    为了方便看,我把没有走的代码先屏蔽掉,第一次put的时候,注意,是第一次调用hashmap.put的时候

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

    我们上面调用构造方法的时候,并没有对table进行初始化,所以它是空,所以肯定进入这个判断,有调一个resize()的方法,这个resize()方法很重要,主要做两个操作,初始化table数组和扩展table数组,第一次调肯定是初始化,我同样先屏蔽部分代码

    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) {
              ......
            }
            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) {
                ......
            }
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
                Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
            if (oldTab != null) {
               ......
            }
            return newTab;
        }
    

    要注意这里定义了几个参数,
    (1)oldTab:变化之前的节点数组
    (2)oldCap:变化前的数组的长度
    (3)oldThr :旧的threshold,也就是默认0
    (4)newCap:记录改变后的数组长度
    (5)newThr :记录改变后的threshold

    数组默认没初始化,所以oldCap是0,oldThr 默认也是0,所以

                newCap = DEFAULT_INITIAL_CAPACITY;   //(1<< 4 = 16)
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // (0.75 * 16 = 12)
    

    可以看出,第一次调用put的时候会先判断数组有没有初始化,如果没有,默认设置长度为16(1向左移动4位),threshold是数组长度的0.75倍(threshold是什么,下面会有说)
    然后赋值给全局变量

            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
                Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
    

    好了,这里调用resize()方法就是为了初始化数组长度,还能从这个源码中看出一种做法,就是你设计某个模块的时候可以不用设计初始化方法,可以在调用的时候再去判断之前有没有初始化,没有就先进行初始化,这样的做法就会显得比较灵活。

    继续看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;
        }
    

    看到有4个参数,tab是形参,就是节点数组,p就是记录某个节点,n是记录数组的长度,i是记录某个下标。
    刚才因为第一次Table为空,所以走进这个判断

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

    调用上面resize()方法初始化数组之后,tab得到一个长度为16的数组,n等于16。
    说实话,我很不喜欢赋值操作和逻辑操作写在一起,感觉这样写不好看
    之后往下看,做判断

    if ((p = tab[i = (n - 1) & hash]) == null)
    

    看到&操作,肯定是位运算,n-1就是1111,用1111去和hash,一个32位的二进制做与运算,得出的结果就是下标。简单来说就是用某个方法生成一个0—15之前的一个小标,比如生成5,那结果就是tab[5]的值是不是空
    所以第二个问题来了,生成下标为什么要用长度-1和hash做与运算
    由于我们这个第一次肯定是空的数组,所以为空

    tab[i] = newNode(hash, key, value, null);
    

    为空的话就新建一个节点赋值给这个数组的下标。然后赋值逻辑就走完了,看看后续的逻辑

            ++modCount;  // 操作数+1
            if (++size > threshold)  
                resize();
            afterNodeInsertion(evict); // 提供给子类重写来给子类在该步骤下做特定操作
    

    中间的操作,键值对的长度加1,如果键值对的长度超过threshold,则对数组进行扩展
    到这里应该就能看出threshold的左右,相当于是一个阀值,如果键值对的数量超过这个阀值,我们就扩展数组,这是java里面HashMap扩展长度的一个思想。

    那么第一次put我们讲完了,其实还挺简单的,假如我们put第二个参数。

        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)
                ....... // 初始化的情况上面讲过了
            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;
        }
    

    根据这个Key的hash,用i = (n - 1) & hash算出下标,如果这个下标下的值不存在,那就创建这个节点和放到这个下标中,比如tab[6],我们和上面一样,创建节点放入数组就结束,很简单。但是假如算出是5,tab[5]在第一次put的时候已经赋值了,那我们这里就要走else

            if ((p = tab[i = (n - 1) & hash]) == 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;
                }
            }
    

    有两个参数,e表示记录节点,k表示Key。这命名,我得吐槽一下,咋不整个aaa呢
    判断

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

    如果当前的hash值和之前存的节点的hash值相同并且key也相同,那就e = p;然后

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

    把这个节点的value换成传进来的value,没错,这步就做了替换操作。
    如果hash不等或者key不等,然后判断p instanceof TreeNode,这个节点是否是红黑树的数据结构。本章不讨论红黑树的情况,你只要知道在某个节点链表长度到一点值的时候,这条链表会转成红黑树就行。
    假如当前节点的数据结构不是红黑树,

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

    死循环,e指向p的下一个节点(怕你乱多啰嗦一句,p就是数组中某个下标的那个节点,也就是链表的头节点)。如果e等于空(说明e处于链表最后一个节点的next的情况),新建一个节点加入链表

    p.next = newNode(hash, key, value, null);
    

    然后判断是否需要把链表转成红黑树

    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
        treeifyBin(tab, hash);
    

    这里就印证我上面说的,当这条链表长度大于等于7的时候,链表会转成红黑树,因为我说了这篇不讨论红黑树,所以我假设长度没到7
    如果没到尾,e!=null,判断e的hash和key与传进来的相不相同,相同的话覆盖(这里的break跳出死循环能走下面的赋值操作)

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

    那假如key也不相等,就把指针移到下一位

    p = e;
    

    然后循环操作。

    总结:put方法中,先判断节点数组初始化没有,没初始化的话会先初始化,然后判断数组某个下标是否为空,为空的话直接创建一个节点给这个下标,不为空的话,循环这个数组下标下的节点的链表,判断是否链表中的某个节点key相同,相同的话覆盖,不相同的话创建一个节点添加到链表的最后,如果长度到达7,将链表转成红黑树。put操作完成之后,最后判断size的长度有没有超过阀值,如果超过阀值,则扩展数组。

    5.数组扩展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
                ......
            }
            if (newThr == 0) {
               ......
            }
            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;
        }
    

    oldCap在第一次扩展的情况下是16,大于0

    newCap = oldCap << 1
    

    新的数组长度就是旧的长度向左移动一位,就是32。每次扩展都是左移一位,所以扩展肯定是2的倍数

    newThr = oldThr << 1; 
    

    这玩意就没这么好算,12左移一位,要把12换成2进制比16换成二进制麻烦,有个更好的方法是拿newCap * 0.75 = 24,你去算也是相等的。那么新的阀值就是24。然后进入循环

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

    for循环是循环数组,如果当前下标没有next,那么这个新数组的这个下标的值就等于旧数组当前下标的值。如果这个下标的节点是红黑树(就是之前链表长度达到7),那就对红黑树做操作(这里不讲红黑树,大概了解就行)。如果都不是(说明当前数组下标的节点是个长度小于7 的链表)

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

    这个操作是,对链表进行遍历,按照e.hash & oldCap这个条件把一条链表拆分成high和low两条链表,把low链表放到新数组的当前下标,high链表放到新数组当前下标+旧长度的下标。
    举个例子,旧的长度16,tab[5]的长度为6,它分成长度为2的low和长度为4的high,新数组tab[5] = low(长度2),tab[21]=high(长度4)
    那么我的第三个问题来了,一分二的意图其实很容易理解,但是为什么要根据这个条件来分?

    6. get方法

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

    根据这个key可以用(n - 1) & hash来拿到下标,put的时候也是这个条件,拿到下标之后判断当前这个下标的节点是否为这个key,是的话直接返回它的value,不是的话判断是不是红黑树,不是的话遍历链表来找相同的key,遍历完还没有的话就返回空。

    7.总结

    (1)主要讲了put、get和resize这3个方法
    (2)hashmap中有两个神奇的操作,第一是扩展数组,第二是把链表转成红黑树
    (3)提出几个问题,如果有大佬知道,请帮忙解答
    为什么获取hash的时候要与自身的右移动16位做异或运算
    为什么根据key生成下标的做法是 (n - 1) & hash
    为什么扩展数组的时候拆分链表的条件是e.hash & oldCap

    相关文章

      网友评论

        本文标题:HashMap源码分析(一)

        本文链接:https://www.haomeiwen.com/subject/bbwggctx.html