HashMap2

写在前面:
由于今年疫情的原因,再加上学业的繁重和找工作的艰难准备,csdn个人博客的文章从去年放寒假到现在一直没有更新过了,在这半年的时间里,自己对于所学的知识有了一个更加清晰的认知,发现以前的文章有很多错误,难免有误人子弟的嫌疑,所以在接下来的时间我会将前面文章的错误一一改正,并继续写出高质量的技术文章,欢迎广大网友的批评与指正。
这一篇HashMap标志着我的csdn个人博客继续更新了,哈哈。

HashMap基本是每一个Java程序员都会用的一个key-value键值对集合,很多小伙伴说HashMap里面的内容太难了,今天我袁非非就不信这个邪,誓要将HashMap弄清楚。以下是正文,有点硬核,小伙伴们,are you ready。

1.HashMap几个重要的属性(JDK1.8):

由于HashMap的内容太多,想要每一行代码,每一个方法都完全弄明白,其实也没有必要,现在就以其中作者认为重要的方法,属性拿出来闹闹磕,如有不正确的地方,欢迎批评指正。

  /**
     * 初始化容量 - 必须是2的次幂.  默认初始值为16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 最大容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 加载因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     *链表转红黑树时冲突链表节点的个数
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 红黑树转链表时 冲突节点的个数
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     容器可能被树化的最小表容量。也就是说链表转红黑树的条件是:冲突链表的节点个数达到8,
     并且HashMap中table的长度达到64
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

HashMap 的基本数据结构图示:

 这个图实在是懒的画,是在网上copy的。有了这个大概图示,然后我们在来看代码层面的数据结构与实现吧。

以上就是HashMap的基本属性和数据结构示意图:接下来我要阐述几个常见的问题,一大波干货来袭。are you ready!!!

1.HashMap的容量为什么是2的次幂:
因为table的长度n永远是2的次幂,那么n-1的二进制表示就是低位一连串的1,例如:0000 1111,0011 1111,当hash&(n-1)时,实际上就是取hash的低m位,2^m=n,那么就会保留后x位的1,例如: 00001111(n-1) & 10000011(hash) = 00000011
这样做的好处有3个:
1.&运算速度快,至少比%取模运算快
2.能保证计算出的索引在capacity中,不会数组越界
3.当n为2的次幂时满足:hash&(n-1)=hash%n

2.加载因子为什么是0.75:
HashMap源码中,我找到了以下注释:

 * parameter of about 0.5 on average for the default resizing
     * threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)). The first values are:
     *
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million

使用随机哈希码,节点出现的频率在hash桶中遵循泊松分布,同时给出了桶中元素个数和概率的对照表。
从上面的表中可以看到当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为加载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。
加载因子是表示Hash表中元素的填满的程度。加载因子越大,填满的元素越多,空间利用率越高,但冲突的机会加大了。反之,加载因子越小,填满的元素越少,冲突的机会减小,但空间浪费多了。冲突的机会越大,则查找的成本越高。反之,查找的成本越小。因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷。

3.java8中为什么要将链表转成红黑树:
链表的插入效率很高,但是查询效率较低,完全平衡二叉树的查询效率很高,但是插入效率很低,于是在链表和完全平衡二叉树之间做了一个折中,采用的是红黑树。

查询效率:完全平衡二叉树>红黑树>链表
插入效率:链表>红黑树>完全平衡二叉树

4.HashMap为什么会线程不安全:

HashMap的线程安全问题主要体现在put操作里面,会有数据覆盖的问题。
主要发生线程安全的代码是这一行:

 if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

我们假设2个线程同时来进行put操作,并且key的hash值相同(产生了hash冲突),那么都会找到在table上的相同的索引位置,在此索引位置上没有元素。假设线程1执行完了if ((p = tab[i = (n - 1) & hash]) == null)里面的内容,并且条件成立,此时cpu时间片用完,线程1挂起。线程2也执行了if ((p = tab[i = (n - 1) & hash]) == null),并且条件成立,那么线程2就会完成tab[i] = newNode(hash, key, value, null);这一步的赋值操作。线程2完成了操作后,线程1此时获得了时间片,继续执行tab[i] = newNode(hash, key, value, null);这一步操作,也会执行成功,此时线程1就将线程2的数据覆盖了。

5.当HashMap的key是Object对象时,为什么需要重写equalshashcode方法:
在HashMap查找的过程中,使用的是key的hashcode和key的equals方法进行查找,先根据key的hashcode计算出hash值,然后再根据hash&(n-1)定位到table中的索引位置,如果发生了hash冲突,需要遍历链表或者红黑树来查找该元素,那么则使用key的equals方法进行查找,如果是简单字符串类型,那么则不必重写equals方法,如果是对象的话,如果不重写equals方法,那么比较的是对象地址,而不是值。在Object中类的equals方法:

public boolean equals(Object obj) {
    //this - s1
    //obj - s2
    return (this == obj);
    }

Node<K,V>节点类(链表)

 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;      //Hash值    并不直接就是HashCode
        final K key;          //key
        V value;             //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;
        }

TreeNode<K,V>节点类(红黑树)

 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // 父节点
        TreeNode<K,V> left;     //左子节点
        TreeNode<K,V> right;  //右子节点
        TreeNode<K,V> prev;    // 删除时需要断开next链接
        boolean red;         //是红节点还是黑节点

重要的方法:

1.计算Hash的相关代码方法

//计算key的Hash值
 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
//根据Hash值计算hash桶的索引位置  n是table的长度   hash是根据key计算出来的hash值
(n - 1) & hash

以上方法是计算key的Hash值的方法:(h = key.hashCode()) ^ (h >>> 16) 获取key的hashcode(32位),将hashcode右移16位然后与hashcode做亦或运算。这里引申出一个问题:为什么要无符号右移16位呢?
举个简单的例子:就以初始容量16为例吧。

h=key.hashcode()      1111 1101 1101 1111 0101 1101 0010 1111
^                    
h>>>16                0000 0000 0000 0000 1111 1101 1101 1111 
-------------------------------------------------------------
hash=h^h>>>16         1111 1101 1101 1111 1010 0000 1111 0000



hash                  1111 1101 1101 1111 1010 0000 1111 0000  
&
16-1                  0000 0000 0000 0000 0000 0000 0000 1111
-------------------------------------------------------------
hash&(16-1)           0000 0000 0000 0000 0000 0000 0000 0000 

通过以上分析,我们可以看出,当HashMap的数组长度比较小时,那么hash值的高16位很可能被数组长度的二进制码屏蔽掉,那么hash值的高16位就完全失去了作用,如果使用(h = key.hashCode()) ^ (h >>> 16)
这种方式,就能够充分利用高16位的特征,降低hash冲突。
那么问题又来了(为什么总有这么多的问题,哈哈):为什么使用 ^ 进行运算,而不是使用 | 或者是 & 进行运算呢?因为 ^ 运算能最大程度的保留数据的特征,使用 | 或则 & 运算会使得数据向1或者0靠拢,失去原有的特征。

2.get()方法的流程

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


     /**
     * 正儿八经的获取Node节点的流程     
     * @param hash  根据这个key计算的hash值
     * @param key  要查找的key  
     * @return the node, or null if none
     */

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


     
get()方法的流程分2种情况:
  • 获取不到数据:

  • 1.table没有被初始化(从这里我们也可以看出HashMap是懒加载的)

  • 2.table初始化了,但是初始化的容量为0(手动指定容量为0)

  • 3.根据这个hash&(table.length-1)计算出的位置上的Node为空

  • 获取到数据:

  • 前提:没有出现以上三种情况

  • 1.根据hash&(table.length-1)计算出的位置上的Node的hash值等于方法传进来的hash值,并且该节点的key等于传进来的key。

  • 2.如果根据hash&(table.length-1)计算出的位置上的Node没有满足1,并且node.next!=null,那么则说明产生了hash冲突,冲突的结果要么是链表,要么是红黑树。

  • 3.如果是链表则遍历链表,判断每一个节点的hash值是否等于传进来的hash值并且key等于传进来的key,找到则返回该node节点。

  • 4.如果链表已经树化了,那么就按照红黑树的方式去查找(红黑树的操作比较复杂,这里就不展开了)。

3.put()方法的流程

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

 /**
     *正儿八经的put的流程
     *
     * @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.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //table未初始化(可以看出HashMap是懒加载)
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
            //table初始化了 但是没有产生hash冲突
            //就是这一行代码会造成线程安全问题,会有数据覆盖的问题
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //产生了hash冲突   
            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;
                }
            }
            //如果这个key映射已经存在,那么使用新的vallue替换旧的value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                    //这个方法是LinkedHashMap里面的,按照访问顺序实现LRU,在HashMap里面只有声明,但是并没有实现该方法
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
            //这个方法是LinkedHashMap里面的,按照插入顺序实现LRU,在HashMap里面只有声明,没有实现。
        afterNodeInsertion(evict);
        return null;
    }





    
put()操作流程主要分为4步:
  • 1.如果table没有初始化,那么就要先初始化table,然后在执行以下逻辑
  • 2.如果table初始化了,并且根据hash值计算出的索引位置上为null,那么则说明该位置还没有被占,则将该hash和key封装成一个Node放在该位置上
  • 3.如果根据hash值计算出的索引的位置上的Node不为null,那么则说明产生了hash冲突,冲突的结果要么是链表,要么已经树化。
  • 4.如果已经树化,那么则执行红黑树的插入逻辑(比较复杂,这里不再展开)。
  • 5.如果是链表,那么则遍历该链表,将hash,key封装成一个Node放到链表的末尾。如果这个key的映射已经存在了,那么使用新的value替换掉旧的value。

4.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;
        //原table中已经有值
        if (oldCap > 0) {
        //元素超过容器最大容量限制,不再进行扩容,直接返回
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //扩容成原来的2倍    
            //有个条件:原来的容量>=默认的容量
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //在构造函数中我们知道,如果创建HashMap的时候没有指定initial capacity,那么这个值就会被初始化为0。
        //如果指定了这个值,那么会被初始化成大于他最近的2的指数,比如14 15 ,都会被初始化为16 会满足2的次幂。
        //这里的意思是,如果在构造函数中指定了这个initial capacity,那么就使用它来作为新的table的实际长度
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
            //如果构造函数中没有指定initial capacity  那么就使用默认值16
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //计算指定了initial capacity的新的threahold
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        //初始化table或者扩容  都是通过以下代码实现的   实际上是新建一个table
        @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;
                //table中存放的是Node节点的引用   此时这个e指向这个节点的引用
                if ((e = oldTab[j]) != null) {
                      //将原table中的Node置空
                    oldTab[j] = null;
                    //如果原来table的位置就只有一个Node元素,也即是没有发生冲突
                    if (e.next == null)
                        //那么就重新hash 根据新的table的长度计算索引  并将原表的Node给新表的位置
                        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;
    }

以上这个扩容的方法可能有点复杂,我们拆开来看。
首先看第一部分:

  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;
            }
            //没超过最大值  扩充为原来的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }

         //在构造函数中我们知道,如果创建HashMap的时候没有指定initial capacity,那么这个值就会被初始化为0。
        //如果指定了这个值,那么会被初始化成大于他最近的2的指数,比如14 15 ,都会被初始化为16 会满足2的次幂。
        //这里的意思是,如果在构造函数中指定了这个initial capacity,那么就使用它来作为新的table的实际长度
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
            //如果构造函数中没有指定initial capacity  那么就使用默认值16
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //计算指定了initial capacity的新的threahold
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        

首先我们必须要知道一个值的作用,就是:threshold。
这个值的作用就是用来判断HashMap是否需要扩容的一个标志,threshold=capacity * load factor(0.75),如果HashMap.size>threshold的话,那么则需要执行扩容逻辑。
这个部分的代码主要做了2件事:第一则是计算新表的大小。第二则是计算新表的threahold。

再来看第二部分:

 Node<K,V> e;
                //table中存放的是Node节点的引用   此时这个e指向这个节点的引用
                if ((e = oldTab[j]) != null) {
                      //将原table中的Node置空
                    oldTab[j] = null;
                    //如果原来table的位置就只有一个Node元素,也即是没有发生冲突
                    if (e.next == null)
                        //那么就重新hash 根据新的table的长度计算索引  并将原表的Node给新表的位置
                        newTab[e.hash & (newCap - 1)] = e;
                        //如果原节点是红黑树节点   那么则按照红黑树的方法进操作(这里不做展开)
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        //如果是链表  这段代码单独来说

这部分代码主要解决了2个问题。第一个是没有发生Hash冲突时直接rehash,然后将节点给到新的表。第二个问题就是处理红黑树(这里没有展开)

最后来看最精妙的代码:

                                resize时候的链表拆分



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

我们再拆分,再做进一步的详细剖析:
第一段:

        Node<K,V> loHead = null, loTail = null;
        Node<K,V> hiHead = null, hiTail = null;
        Node<K,V> next;

上面的代码定义了2个链表,我们称之为:lo链表和hi链表,loHead和loTail分别时lo链表的头和尾;hiHead和hiTail分别时hi链表的头和尾。

第二段:

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

我们再将以上代码框架抽象出来:

   do {
         next = e.next;
      } while ((e = next) != null);

我们可以从这个结构看出,这段代码就是顺序遍历旧的table上的链表的节点

我们再来看里面的if…else:

                       if ((e.hash & oldCap) == 0) {
                                 // 将节点插入lo链表
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            //将节点插入hi链表
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }

上面的代码就是说,我们原来table的一条链,现在被拆分成了2条链,原来一条链上的元素现在分布到这2个链上。这个元素分布到hi链还是lo链的标准就是 e.hash & oldCap) == 0e.hash & oldCap) != 0,如果e.hash & oldCap) == 0,那么Node就被划分到了lo链,如果e.hash & oldCap) != 0,那么就被划分到了hi链。通过以上方式,原先旧的table索引位的一条链表的元素,现在就被拆分到了新的table的2条链中。

最后我们再来看第三段:

                       if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }

第二段代码我们已经将链表的元素划分到了2个链表中,那么这2个链表的元素如何处理的呢,就是通过以上代码实现的:这段代码看上去就很简单了,就是将新的2条链表放在新的table的jj+oldCap的位置,j就是指旧的链表在table的索引位置,oldCap指的是旧的tabl的长度。

以下内容将对e.hash & oldCap) == 0j j+oldCap做一些解释说明:
首先我们要明确几个点:
1.oldCap一定是2的整数次幂,2^m。
2.newCap是oldCap的2倍,2^(m+1)。
3.获取hash值的方式为:(n-1)&hash,实际上就是取hash的低m位。

对于扩容后的hash,那么就是取hash的低m+1位,对于同一个hash(每个元素的hash值相同)来说,低m位和低m+1位的区别在于要么相同,要么m+1=m+1000....(oldCap)。实在不好说明白,举个例子吧。
假设table大小为16,那么就是2^4,取元素hash值低4位就是:假设是abcd,那么扩容后的table的大小是32,那么就是 2^5,取hash值的低5位,这里有2种可能:1abcd或者0abcd,此时低5位和低4位有如下关系:0abcd=abcd , 1abcd=abcd+10000 即有:1abcd=abcd+oldCap,此处的abcd就相当于j,旧的table中的位置。

以上就差不多解释了:(e.hash & oldCap) == 0这行代码的精妙,由于作者语言水平有限,如有解释不当,请读者自行斟酌,或给我留言评论。

作者感言:
这是今年的第一篇文章,也是我认为写的比较全面的一篇文章,自己的理解再加上参考了几位大佬的博客才写出来的。这一篇文章写了一周呀,利用中午上班的休息时间,作者精力旺盛,中午都不需要休息的,话说实习也没啥太多事情,所以跑这来写文章来了,哈哈,都是为了秋招呀,希望能够有个好的offer,奥里给。以后每篇文章我都会写一个作者感言,也聊聊生活,生活不只有代码,还有诗和远方。

如果觉得好,请不要吝啬你们的大拇指,点个赞,嘿嘿嘿。!!!

你们的给力,就是我的动力,我是爱生活的袁非非!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值