JDK1.8 HashMap源码

HashMap底层数据结构是数组 + 单向链表 + 红黑树

HashMap底层数据结构.png

一、相关概念

1、Hash冲突:就是在一个数组的位置上出现了一个链表,这就是所谓的hash冲突。

2、解决hash冲突,就是让链表的长度变短,或者干脆就是不产生链表,一个好的hash算法应该是让数据很好的散列到数组的各个位置,
   即一个位置存一个数据就是最好的散列,下面说的链地址法,说的就是在hashmap里面冲突的时候,一个节点可以存多个数据。

3、桶(bucket):数组中的每一个元素的位置就是一个桶。

4、HashMap的初始容量:16,并且 HashMap的底层数组长度总是2的n次方
           static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
 
   HashMap的最大容量:2的30次方
           static final int MAXIMUM_CAPACITY = 1 << 30;

5、 HashMap的加载因子是:0.75f
           static final float DEFAULT_LOAD_FACTOR = 0.75f;

         如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;
         如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下我们是无需修改的。

6、将链表转为红黑树设置的链表长度的阈值:8
          static final int TREEIFY_THRESHOLD = 8;

7、 从源码中可以看出,每次新建一个HashMap时,都会初始化一个table数组。table数组的元素为Node节点。
    Node为HashMap的内部类,它包含了键key、值value、下一个节点next,以及hash值,正是由于Node才构成了table数组的项为链表。

8、创建一个Node类型的数组,transient 意思是不能序列化
          transient Node<K,V>[] table;

二、计算key在数组中的位置

1、获取key的hash值
     (h = key.hashCode()) ^ (h >>> 16)
   
     ● 通过hashCode方法获取到key的hash值,然后把hash值的低16位与高16位进行异或运算得到的值即为该key的hash值
  
     ● 原因:
           因为后面要使用 (n - 1) & hash,所以通过hashCode方法获取的hash值不同也可能数组下标相同。为了降低运算结果值的相同概率,
        使table数据分布均匀,减少碰撞

2、计算key在数组中的位置 【n是数组的容量,即长度】

   ①、对于HashMap的table数组而言,数据分布需要均匀(最好每项都只有一个元素,这样就可以直接找到),不能太紧也不能太松,
      太紧会导致查询速度慢,太松则浪费空间。

   ②、计算hash值后,怎么才能保证table数组中元素分布均与呢?我们会想到取模,但是由于取模的消耗较大,
      HashMap是这样处理的:采用按位与运算,即  (n - 1) & hash,效率高,可以均匀分布table数据和充分利用空间。   
 
   ③、原因:
       ●  (n - 1) & hash   与   hash % n 是等值不等效 ,  (n - 1) & hash 的效率高于 hash % n ,这是HashMap在速度上的一个优化。

       ●  按位取与,作用上相当于取模mod或者取余%。

★ 为什么 HashMap的底层数组长度总是2的n次方?

  ● 数组的默认长度是16,用二进制表示为:    10000
    (n - 1) 即 (16 - 1) , 用二进制表示为:01111

    因为数组长度总是2的n次方,所以如果扩容一次之后,数组的长度为32,用二进制表示为:100000
                            扩容一次后,(n - 1) 即 (32 - 1),用二进制表示为: 011111

    然后使用(n - 1) 与 key的 hash 进行 & 操作,结果是由key的hash值决定的(hash值是一个整型)

  ● 如果数组的长度不是2的n次方,假如数组的长度为15 ,则用二进制表示为:011111
                        则 (n - 1) 即 (15 - 1) , 用二进制表示为:011110

    然后使用(n - 1) 与 key的 hash 进行 & 操作,因为数组长度不是2的n次幂,所以 (n - 1) 转换为二进制最后一位永远都是0 ,
    & 的结果不能只由key的hash值决定的(hash值是一个整型),所以空间减少,产生hash碰撞的概率就高

    例如数组长度为9,hash值为3,计算其在数组上的位置为: 3 & (9 - 1) = 0  -->   0011 & 1000 = 0
        数组长度为9,hash值为2,计算其在数组上的位置为:2 & (9 - 1) = 0   -->   0010 & 1000 = 0  在数组的相同位置上,发生碰撞;

    例如数组长度为8,hash值为3,计算其在数组上的位置为:3 & (8 - 1) = 3   -->   0011 & 0111 = 0011      
        数组长度为8,hash值为2,计算其在数组上的位置为:2 & (8 - 1) = 2   -->   0010 & 0111 = 0010   不同位置上,不碰撞;

▲ 总结:数组的长度必须是2的n次幂,当length = 2^n时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,
       查询速度也较快。

三、HashMap的put方法执行过程可以通过下图来理解:

HashMap的put方法.png
①、调用put方法,首先判断数组table是否为null或数组的长度是否为0,如果是,就执行resize()进行扩容,转向②,如果不是,转向②;

②.根据键key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③.判断table[i]位置的key是否和要添加的key相同(通过hashCode以及equals进行比较),如果相同直接覆盖旧的value,添加新value,
  并返回旧的value,否则转向⑥,,如果不同,转向④;

④.判断table[i] 是否 instanceof TreeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,转向⑥,否则转向⑤;

⑤、遍历table[i]位置的链表,判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;
   遍历过程中若发现key已经存在直接覆盖value即可;

⑥.插入成功后,数组长度加1,然后判断实际存在的键值对数量size是否超过了临界值threshold(数组容量 * 加载因子),如果超过,进行扩容。

四、HashMap的resize方法执行过程:

★ resize()方法的作用:
                 初始化数组Node; 
                 对数组进行扩容

①、获取原来数组的长度oldCap(oldCapacity),进行判断:

       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
        }

②、如果数组长度oldCap > 0,判断数组长度oldCap是否 > 数组长度的最大值(2的30次方),如果大于,则不进行扩容,返回原来的数组oldTab;
     
   否则判断 oldCap << 1是否大于数组长度的最大值(2的30次方) 并且 oldCap是否 > 数组长度的最大值(2的30次方) ,
        如果不大于,则进行扩容,即扩容后的数组长度为原来数组长度的2倍,临界值threshold也要左移1位,扩大到原来的2倍

③、如果数组长度oldCap <= 0,则对数组进行初始化操作,数组默认长度为16,临界值threshold(数组容量 * 加载因子)为12(16*0.75);
        newCap = DEFAULT_INITIAL_CAPACITY
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY) 

④、创建新数组newTab,数组容量为扩容后的,即旧数组容量的2倍

⑤、判断旧数组是否使null,如果不是,则遍历旧数组,如果oldTab[i] != null,则把oldTab[i] = null 【把旧数组oldTab[i] 置空】

       ●  判断oldTab[i] 下面是不是null [即 e.next == null],如果不存在,则重新计算oldTab[i] 在新数组中的位置

       ●  判断oldTab[i] 下面是一个红黑树,则把该红黑树进行split(分割),然后重新计算在新数组中的位置

       ●  判断oldTab[i] 下面是一个链表(该链表的长度不会超过8),则进行循环遍历,新数组中元素的位置只可能在两个地方,
           一个是原下标的位置,另一个是下标为 [原下标 + 原容量] 的位置

                ●   如果 (e.hash & oldCap) == 0,则新数组中元素的位置在原下标的位置

                ●   如果 (e.hash & oldCap) != 0,则新数组中元素的位置在下标为 [原下标 + 原容量] 的位置

⑥、返回新数组newTab
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容