面试必备系列之HashMap源码解析,来自HashMap的灵魂拷问,你想要的全都有

什么是Map?

map就是用于存储键值对(<key,value>)的集合类,也可以说是一组键值对的映射(数学概念),它也是java中的一个顶级接口,下面有许多我们常用的map子类,如hashmap,concurrenthashmap等。

 

HashMap解析

数据结构(以1.8之后的HashMap结构为例子)

组成HashMap的结构为数组+线性链表+红黑树(1.8新增)。

我们以下面这段代理的运行为例子,讲一下HashMap结构。

/**
 * @Author Dark traveler
 * @Note 我心净处,何处不是西天。
 * @Descrption
 * @E-Mail : 1029149772@qq.com
 * @Date : Created in 10:39 2020-3-27
 */
public class HashMapDemo {
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>(13);
        map.put("author","Dark traveler");
        System.out.println(map.get("author"));
    }
}

(1)我们首先会去new一个数组。

为什么HashMap选择用数组?

*因为数组效率高,它可以通过索引下标很快找到我想取的数据,基本上一次就能定位到你数据所在的位置,时间复杂度为O(1)。.

(2)调用put方法插入键值对

它通过key这个对象的hashcode方法(正负都有可能)来计算出key对象的hashcode值,然后用这个值对第一步new出来的数组长度进行取模运算来得到键值对的索引。

*公式hashcode%hashmap.length<hashmap.length(我们猜想的公式,但实际是用位运算完成)

既然是取模运算,那么很有可能算出相同的索引值,这样怎么办呢?

所以,这里就引入了HashMap的第二种结构,链表,当通过取模运算得到相同的数组索引,那么它们在数组中的位置也相同,此时就会调用equcals方法,现比较它们的key的hashcode,如果不相同,就以链表的形式挂在下面。

那如果key的hashcode相同呢,我们都知道hash计算的空间利用率并不是那么高,所以当hashcode被算出来的时候,是有可能出现两个对象用同一个hashcode的情况,这也就是所谓的哈希碰撞,当索引相同时,随之比较链表中key的hashcode,当hashcode也相同的时候,就会把新的value值覆盖旧的value值。

(3)调用get方法获得key的value

*最完美的情况,并没有产生碰撞,那么所有的键值对都是平均分配在数组中,那么取值只需通过索引定位一次就能够取到,速度很快,时间复杂度为O(1)。

*产生碰撞的情况,取得的索引中形成了链表,我们都知道链表是这样一种结构,插入快,只需要改下元素得指针,但是查询却要通过遍历来查询,所以这样就导致了当查询到形成链表得那一块,时间复杂度为O(n),如果此时链表的长度为1千,1万,那么就大大的降低了效率。

*所以基于上面这种情况,在jdk1.8中,又加入了红黑树的结构,当链表节点>7的时候,链表结构会自动转换成红黑树,那么为什么是7呢?因为红黑树深度如果不够的话,反而会比较鸡肋,而这个关键节点数量就是7,超过7转换成红黑树就可以优化效率。

红黑树:一种接近平衡的二叉搜索树,在每个结点上增加一个存储位表示节点的颜色,可以是Red或Black,通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,它支持查找、插入、删除等操作,其时间复杂度最坏是O(logn)。

注:关于时间复杂度,我这里从别人博客上找了一张图,可以很好的解释概念。

 

源码解析

分析源码之前我们先从上面的数据结构上面提几个问题,看看我们能不能在源码中来找到答案。

*数组默认初始容量,我们一般new一个HashMap并不会去设置容量,那么它的初始容量是多少呢?

*它的取模运算真得是上面的公式吗?1.7和1.8是否有区别?

*HashMap扩容的负载因子loadFactor为什么是0.75?

*链表转红黑树的阈值真得是8吗?是不是如大部分人所说当链表长度为8的时候就转红黑树呢?

*变了红黑树以后,如果我移除了很多节点,它是不是又会变回链表呢?

*在开发过程中用哪一种初始化HashMap方法最好呢?

好,那么我们先进入HashMap类去看看,通过代码来验证上面那些问题。

运算方法,带左移4位 ,左移四位(左移不分有无符号,都是在后面补0)

00000000 00000000 0000 0001  << 4

00000000 00000000 0001 0000 = 16

1、当我们拉过一大段的注释之后,我们可以看到, HashMap有个final的成员变量,默认初始化容量为 1<<4,后面有个注释,16,没错,HashMap的默认容量是16,并且上面还有个注释,默认初始容量-必须是2的幂。

什么?我刚刚明明传了个13,也没问题不是,我们来看看它在初始化里面做了什么?连续根了三个方法,我们发现了方法tableSizeFor(int cap),我们发现虽然我们传了13进去,但是它通过移位运算把这个13变成了16,也就是变成了比cap大的最小的2的幂次方的值,比13大且是2的指数次幂的值就是16,我们从它的注释中也能看出,Returns a power of two size for the given target capacity,根据你传的初始化容量,返回一个2的指数次幂,所以答案揭晓了,它把我们传的13变成了16,符合2的指数次幂且比13大。

举个例子,如果传进来的cap是13。

n = 13 -1 =12 

12的二进制: 00000000 00000000 00000000 0000 1100

 我们先来分析下

n |= n >>> 1; 先把n无符号右移1位,再进行按位或运算

n>>>1  无符号右移1位,也就是说向右移动1位,最高位补0,不管该数是正是负,那么负数会变成正数

按位或运算概念:相同二进制位上面,都是0则为0,否则为1。

n=12                          00000000 00000000 00000000 0000 1100

n>>>1                       0 00000000 00000000 00000000 0000 110 = 6

-----------------------------------------------------------------------------------------------------

n=12                         00000000 00000000 00000000 0000 1100

n>>>1=6                   00000000 00000000 00000000 0000 0110 

n = n | n>>>1            00000000 00000000 00000000 0000 1110  = 14

-------------------------------------------------------------------------------------------------------

n=14                         00000000 00000000 00000000 0000 1110

n>>>2                      00000000 00000000 00000000  0000 0011

n = n| n>>>2            00000000 00000000 00000000  0000 1111  =  15

-------------------------------------------------------------------------------------------------------

n=15                        00000000 00000000 00000000  0000 1111

n>>>4                      00000000 00000000 00000000  0000 0000

n = n| n>>>4            00000000 00000000 00000000  0000 1111  =  15

-------------------------------------------------------------------------------------------------------

n=15                        00000000 00000000 00000000  0000 1111

n>>>8                      00000000 00000000 00000000  0000 0000

n = n| n>>>8            00000000 00000000 00000000  0000 1111  =  15

-------------------------------------------------------------------------------------------------------

n=15                       00000000 00000000 00000000  0000 1111

n>>>16                   00000000 00000000 00000000  0000 0000

n = n| n>>>16         00000000 00000000 00000000  0000 1111  =  15

你会发现经过这几次移位运算以后,n的值停留在了15,当后4位都是1以后后面的运算就没有意义了。

所以你会发现,它的目的就是要把你传进来的数,取到你这个数二进制为1的最高位,然后把后面全变成1,这也就是为什么它第一步要把你传进来的值减1,如果你传的刚好是2的幂次方,如果不减1它就会向上进一位了,也就是16的话会变成31,显然这不是它想看到的。

最后通过两个三目判断下,第一个三目判断如果你传的是0,就改成1,那二个三目判断如果你的数不是大于1<<30,就让你的数加1,上面的15+1 也就变成了16。

说完了改变数组长度的问题,再看看它把你的这个改变后的长度放到了哪里。

拿到你的值返回到上面,你会发现,它并没有把这个16变成数组的长度,而是把它给了threshold这个变量,并不是我们想象中的new一个长度为16的数组,这是为什么呢?

很不解,我们继续看它的其它构造方法,一般我们都用无参,我们看下无参构造,我们发现它也只是赋值了一个负载因子为0.75而已,那么它到底在哪里new了我们的数组呢。

2、初始化方法看完,我们就看它的put方法。

首先看它的第一个put方法,它把生成的hashcode又进行了一次移位运算,为什么?

我们现在来分析下这个按位异或运算。

按位异或运算概念:二进制位上数字相同就为0,否则为1。

我们这里举一个例子,说明下有这个按位异或的扰动运算和没有的差别。

如果没有这个扰动运算的情况。(数组索引的公式为 hashcode&(数组长度-1),下面有详细讲解)

 加上扰动运算后的情况。

很显然我们会发现,在产生一些差hashcode的情况下,减少了hash碰撞的概率。

好,看完它对hashcode的优化以后,我们回到上面的问题,什么时候new数组?看代码我们发现当这个HashMap中的数组table==null或者数组的长度==0的时候,它会调用一个resize()方法。

那么我们再来看看这个resize()方法,我们把threshold带进去看看,因为初始化,所以并没有这个数组,我们只需要关注数组初始化过程。

 final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;   //table为null,所以oldCap为0
        int oldThr = threshold;    //threshold = 16 = oldThr
        int newCap, newThr = 0;
        if (oldCap > 0) {        //第一次进来不大于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;    //大于0,所以 newcap(新数组长度) = 12 = threshold
        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;    //ft = 16*0.75
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);  //newThr = 12
        }
        threshold = newThr; // threshold  = 8
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //重点,newTab【16】  
        table = newTab;  //table = newTab【16】
        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;
    }

 所以我们发现。它是通过putVal这个方法中的resize()方法来new初始数组的,并且第一次确实把threshold赋值给了newCap,也就是确实构造了一个长度为16的新数组。

我们再来看看我们最关心的数组索引的运算。

我们想的取模运算:

index = key.hasCode() % n

HashMap的取模运算:

 n = (tab = resize()).length;
(p = tab[i = (n - 1) & hash])
//看它索引计算方式,哈希值与数组的长度-1来按位与
i = (n - 1) & hash

按位与运算的概念:相同二进制位上面,都是1结果为1,否则为0。

&运算这里举个例子,假设数组长度为8,有两个hash值分别为3和10。

常规公式 hashcode%length      算出的索引为 3 和 2

移位公式 hashcode&(length-1)

当然,其实它们的二进制是32位,我们这里就取后8位,因为前面都是0不会影响。

hashcode = 3 =     0000 0011

length-1 = 7 =        0000 0111

-----------------------------------------------

                               0000 0011 = 3

hashcode = 10 =   0000 1010

length-1 = 7 =        0000 0111

----------------------------------------------

                               0000 0010 = 2

发现没有,它做出的来答案和取模运算是一样的,但二进制层的运算效率比直接的取模要高很多。

好,那么问题来了,作者为什么要把数组的长度设置成2的指数次幂。

我们现在假设数组的长度是7

hashcode = 3 =     0000 0011

length-1 = 6 =        0000 0110

-----------------------------------------------

                               0000 0010 = 2

hashcode = 10 =   0000 1010

length-1 = 6 =        0000 0110

----------------------------------------------

                               0000 0010 = 2

这样就很容易变成算出的索引值相同了,当然我举的这个例子也不符合取模公式了,所以为什么要容量为2的指数次幂,就是增加运算效率和减少出现同位索引的情况。

接着往下看putVal方法。首先,我们存一个键值对进来,分析好索引以后,如果这个数组的索引里面没有值,它会直接new一个node放在这个索引位置中。

如果这个索引位置不为空呢?

首先看第一个条件,索引槽中key的hashcode和你传进来的key的hashcode相等,且这两个key对象的地址相同或者它们equals相同,说白了,就是它们是同一个对象的情况下,会把老的节点替换成新的节点,这里也就说明了为什么当你传相同的字符串形式的key的时候,value会覆盖替换了。

在看第二个条件,如果不符合上面那种情况呢,他会先判断这个原索引槽的节点是不是一个树节点,如果是,在这个树节点后面挂上一个树节点。

如果上面两种情况都不满足呢,那不就是还没变成树,传进来的key对象又不是同一个,那么它就会把节点直接以链表的形式挂到最后一个元素后面。

说白了里面就两个关键点,一个死循环里面,放两个条件,第一条件,如果当前是这个链表的next元素为null,那就把这个元素挂在上面,如果不为null,那就是第二个条件,那么就可以知道这个链表肯定不止一个元素了,那么就找到这个第二个元素,和上面一样比较一下它们的key的hash看下是不是同一个,如果是同一个就直接跳出这个死循环,如果不是同一个就继续比较,一直比较到这个链表的最后一个为止,并且把这个新节点节点挂在最后,跳出,同时第一个条件里面还有一个判断,如果binCount = 7,也就是这个循环做了8次的时候,调用转红黑树方法,然后跳出死循环。

 

 

但是上一步留了一个坑,如果在第二个if跳出,那么e就会等于当前那个和插进来的值key相同的值,它只知道这个以后直接跳出到下一个方法。

如果这个e!=null,也就是那个新节点没有挂载在后面的情况下,它又做了个替换,把当前相同的key的新value,替换老value,同时它会把这个oldValue返回出来,告诉你,这里替换值了。

所以这里有个小tips,当你传的key是同一个的时候,你会发现put方法有返回值哦?它的返回值是你替换的oldValue。

好了,这就是整个putVal方法了,也就是HashMap里面个人认为最经典的方法了。

——————————————————————————————————————————————————--————

 现在我们再回到这个resize()方法,我们来看看HashMap是怎么扩容的。

首先我们来了解这样一个概念,也是运算的,假设我第一次的数组长度是16,有两个hashCode如下,key1和key,我们通过计算索引的公式hashcode&(map.length-1),得到在数组长度为16的时候,这两个hashcode所得到的索引都是5,那么它们毫无疑问在数组索引5的地方形成了链表。

此时我们数组发生扩容,扩容一次达到了32,它扩容就是<<1,左移一位也就是*2,那么我们此时再调用这个公式hashcode&(map.length-1),得到在数组长度为32的时候,这两个hashcode所得的索引一个是5,一个是21,那么就得出一个结论,新数组的索引要不就是原索引,要不就是原索引+原数组长度。

好那么哪一种是原索引,哪一种又是原索引+原数组长度呢。

如果hashcode&原数组长度,得出的结果是0,那么就算是低位,就还是放在原来的索引中,如果hashcode&原数组长度得出的结果不为0,就放到原索引+原数组长度的索引中。

好,我们了解了这个概念以后我们再来看源码。 

第一段我们可以看出,它确实是以左移1位来扩容的,也就是容量*2,然后我们看关键的新算法。 

看到了吧,用这个节点的hashcode和oldCap进行按位&运算,分别得出两个链表,一个是结果为0的,一个是结果不为0的,然后 把结果为0的链表放到原来的索引newTab【j】中,不为0的链表放到newTab【j+oldCap】中,而这个j就是遍历老的数组的当前索引,这样就再也不用rehash,然后重新再进行一波索引的计算了,只需要看它们的高位就行了,大大提高了效率。

————————————————————————————————————————————————————————

 

 好,那除了速度更快它还有啥好处呢,为什么1.8以后要把rehash方法去了,换成这个呢?因为之前1.7中还有一个很致命的原因,在高并发环境下,rehash可能导致死锁,而导致这个致命原因的代码就出在这里(do-while循环中),如下:

void transfer(Entry[] newTable) {  
    Entry[] src = table;                   //src引用了旧的Entry数组  
    int newCapacity = newTable.length;  
    for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组  
        Entry<K, V> e = src[j];             //取得旧Entry数组的每个元素  
        if (e != null) {  
            src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)  
            do {  
                Entry<K, V> next = e.next;  
                int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置  
                e.next = newTable[i]; //标记[1]  
                newTable[i] = e;      //将元素放在数组上  
                e = next;             //访问下一个Entry链上的元素  
            } while (e != null);  
        }  
    }  

为什么说它可能导致死锁呢,举个例子,假设线程一和线程二同时对共享的变量一个HashMap进行扩容,并且同时产生了两个新的长度的数组,此时进行进行计算把新数组的索引指向老数组的索引,该索引上的元素会被线程一的扩容数组和线程二的扩容数组指向,但老数组key里面对象的value只有一份,这里假设这个key中只有两个链表元素,那么总有一个会先把链表元素搬过去,先搬过去的那个扩容数组会把这个链表元素前后节点互换,就是上面那些代码的操作,但是此时后一个指向这个链表元素节点的扩容数组指针却没有改变,它会把前一个扩容数组的节点原封不动的搬下来,这样就造成了这个链表元素头指向尾部,尾部指向头部,形成一个闭环,也就是死锁,所以在1.8中使用了这种新的扩容办法来避免这个问题。

但是虽然避免了死锁它依然是不安全的,虽然它不再是倒插式放到新数组中,但依然会存在覆盖问题,举个例子,当线程一已经移动完成,同时对应的链表又插入新值,而线程二中才刚刚扩容完毕并没有新链表节点的指针,那就依然会把刚刚插入链表的新值给覆盖掉。

3、负载因子是0.75的原因,我们先看看原作者的解释,说白了就是一个时间和空间成本的权衡,当你一个数组长为16的时候,它不会当你站满所有槽再来扩容。我们都知道链表长度越长,get的时候复杂度越高(时间换空间),反之,链表越短那么效率确实会提高,查询时间就会短很多(空间换时间),那么通过实验表明,当load=0.5的时候时间开销最佳,当load=1的时候,空间开销最佳。

所以,根据牛顿二项式推导出来log2约等于0.698,作者干脆就取了个折中值0.75。

4、阈值为8的原因,同样我们看看原作者的解释。这里就涉及到一个概率统计的公式泊松分布,作者根据这个公式得出现链表长度为8的时候概率是0.00000006,也就是亿分之六,已经无限趋向于0了,虽然1.8加上了红黑树,但是小数据量很难产生,除非数据量非常庞大,达到十万,百万的时候才可能会慢慢产生,当然前提是加载因子必须是0.75。

但是,事实真得就是这样吗?我们再看到putVal方法的转红黑树的地方,如下图,我们看看这个循环,我们发现它的意思是,当产生了相同的哈希索引的时候,给初始节点的尾部挂上下一个节点,首先要有一个节点站住了这个数组的索引位,才会开始挂载这个相同哈希索引的节点。

 

 那么我们知道了它的想法以后,我们用for循环写下它的结构,看看它结构到底是怎么变化的。

        /** binCount值    链表长度
         *   0              2
         *   1              3
         *   2              4
         *   3              5
         *   4              6
         *   5              7
         *   6              8
         *   7              9
         */
        int length = 1;
        for(int binCount =0;;++binCount){
            System.out.println(binCount);
            {
                //当数组这个位置已经占有一个节点以后,开始挂载链表
                length = 1 + length;
            }
            if(binCount>= 8-1){
               System.out.println("此时链表长度"+length);
                System.out.println("循环运行次数"+(binCount+1));
                System.out.println("调用红黑树方法");
                break;
            }
        }

因为节点赋值的方法在我们判断方法的上面,所以你会发现,当这个地方触发红黑树转变的方法的时候,此时链表的长度应该是9,也就是说数组中有一个节点,它下面挂着8个节点,再回去看看那个阈值,作者的解释分明是从0开始的,看到这里小伙伴们应该明白了吧,阈值为8的时候,链表的长度其实是9啊。当然,看到这里是不是有种恍然大悟的感觉,但事情的真相不止如此,我们再进入转红黑树的方法看看。

天呐!它里面还有一个判断,当数组长度不超过64的时候,我们只是调用了扩容方法,并没有转红黑树,只有超过64的时候,才会调用下面转红黑树的方法。

所以,事情的真相只有一个,当链表的长度为9(包含了数组中的那一个),且数组的长度大于64的时候,此时才会调用转红黑树的方法,而好多地方说什么大于8就转红黑树,千万不能理解为链表的长度为8(本人就听很多人说过这个错误的解释),大于8是作者给出的阈值,而这个阈值是从0开始的,也就是类似于索引0开始一直到8,所以链表的长度为9的节点在1.8以后的HashMap中是看不到的了。

好,假如我们现在都把这些条件满足了,我们来看看这个红黑树是怎么转换的。

你会发现它还说遍历那个链表,把第一个取节点取出来,把它变成一个TreeNode,并且把它变成hd,只会其它的节点都以指针的方式以此挂载,那不还是链表嘛,只是把node变成了treeNode,没错,但是最后它还有个方法,如果这个树的头节点不为null,也就是我们转换链表的第一个节点,既然转换了当然不为null,那就调用treeify方法,把这整个tab传进去,把这段链表改成红黑树。

5、它确实会变回链表,当下面的链表节点<=6的时候,它就会转换回链表,从作者设置的这个值可以看出,这里是为什么我也没有去深究,但想一想肯定无法也是时间和空间效率的平衡,这里我在网上找了一个我所能接受的解释。

6、在阿里巴巴开发手册中有一段这样的描述,HashMap初始化时建议指定它的初始值大小,也就是建议使用带初始值大小的那个初始化方式,为什么呢?

原因:如果不先指定大小,HashMap可能会经历多次扩容,比如你要存几百个key-value,不预先指定大小的话,它会从16一路扩容上来,会触发多次resize方法,引发不必要的消耗。

那么建议怎么选择初始容量大小呢,假如我要存7个值,那么就把初始容量设置为7嘛?

显然是不对的,经过大量的实验,阿里巴巴给出了这样一个公式。

 把7带入,initialCapacity = 7/0.75 + 1 = 10,也就是说你初始值传10是最好的。

如果你传的是7的话,他就便成8了,而8*0.75 = 6,它很可能在存6个的时候就扩容了,所以传10变成16,基本上就不会去扩容。

 

总结:

1、HashMap的默认初始容量是16,而且构造方法中会把你自定义的长度转换成向上的最近的2的n次方幂,它是通过移位算法来帮你转的,移位算法中为什么要把你传的cap-1,是因为防止你传的刚好是2的n次方幂导致它多往高位进一位。

2、HashMap的计算hash索引索引方式是hashcode&(table.length - 1),是一种更快的取模运算,也是为什么要把数组的长度定为2的n次方幂的原因,这样可以通过这个运算增加运算效率,在jdk1.8中还加入了一个扰位运算,把hashcode无符号向右移动16位再与原hashcode进行按位异或运算,减少hash碰撞概率。

3、HashMap的扩容方法,从1.7的rehash(从新计算一遍hash索引),改成了现在的resize()方法,主要是去除了transfer()方法,新的高位索引通过原位置+原数组长度来得到,同时解决了产生死锁的问题,而且1.8中的初始化table长度方法也放到了put中,不再是初始化的时候就new一个数组了,而是调用put的时候再去生成你要的数组。

4、负载因子7.5的由来是通过牛顿二项式计算而来的。

5、HashMap在1.8中,转红黑树的阈值是8,但是实际的链表长度为9(如果不算上数组中的那个节点才是8)且数组长度大于64的时候才会转成红黑树,而转成红黑树以后,当红黑树的链表节点少于或等于6位的时候,红黑树又会转化回链表。

6、工作中,初始化HashMap最好的方式就是,传入 (需要存储的个数/负载因子+1)的公式算出来的初始容量大小来对HashMap进行初始化,最大程度的避免扩容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值