jdk1.8 HashMap源码解析(上)

1、HashMap概述

Map 是 Key-Value 对映射的抽象接口,该映射不包括重复的键,即一个键对应一个值。HashMap 是 Java Collection Framework 的重要成员,也是Map族(如下图所示)中我们最为常用的一种。简单地说,HashMap 是基于哈希表的 Map 接口的实现,以 Key-Value 的形式存在,即存储的对象是 Entry (同时包含了 Key 和 Value) 。特别地,\color{red}{HashMap最多只允许一条Entry的键为Null(多条会覆盖),但允许多条Entry的值为Null。}此外,HashMap 是 Map 的一个非同步的实现。

2、设计思想

HashMap采用的是哈希表的数据结构。哈希表是一种以\color{red}{键值对}来存储的数据结构,哈希表的哈希函数通常有 直接定址法、平方取中法、求余法等。
而hashMap的设计者则采取求余法来对其进行元素的均匀分配。
在源码中,有这么一句代码tab[i = (n - 1) & hash],n代表的是数组的长度,hash是指的key的hashCode()值,这句话设计者的意思是想让其相当于hash%n(位运算比%的运算要快,源码中有大量这种位运算),那么你可能会问了,怎养才能使得这两个相等呢?这就涉及到位运算的知识了。
首先要知道1&1=1,0&0=0,1&0=0,0&1=0

要使得 (n-1)&hash <==> hash%n,就得使得n为2的n次幂,因为2的n次幂的二进值形式为0000000100000xxxx,因为hashmap的数组长度初始化为1<<4,也就是16,那么就以16为例子,二进制为0...010000 ,n - 1也就是 0...01111,那么(n - 1)& hash为

hash :              00101010 00011111 00101010 01110100
                                                        &
n - 1:              00000000 00000000 00000000 00001111
---------------------------------------------------------
                    00000000 00000000 00000000 00000100
(可以看出得到的值是在0-15之间(这就保证了==>hash%n), 而且决定最终值的是hash的后面4位)
    这是hash值的计算方式。
     static final int hash(Object key) {
        int h;
        //将前面的16位和后面的16位进行异或,使扰动之后的index结果尽可能的不同
        //这么做也能在保证table的length比较小的时候,也能
        //保证到高低位的bit都参与到hash的计算中来,同时也不会有太大的开销
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
注意,由于hashCode()可以进行重写(hashCode和equals()这两个方法一定要
一起重写),所以key的hash值有可能重复(另外字符的hashCode()也有可能重复,如一些特殊字符)

那么你又会问了HashMap是如何保证长度是2的n次幂的呢?
这就要看tableSizeFor()了,这个方法保证了数组table的长度是不小于cap的最小2的n次幂

static final int MAXIMUM_CAPACITY = 1 << 30;//超过这个,就变成负数了
//得到不小于cap的最小整次幂(最高位1后面都变成1,然后再加1)
//eg:1 2 4 8 16 32
//cap:5 15 30    传入的cap
//n:8 16 32      得到的n
static final int tableSizeFor(int cap) {
    int n = cap - 1;//为了避免cap是2的整次幂,从而导致n变成cap的2倍
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : 
    (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
总体思路是:因为2的幂次都是1000xxxx的形式,所以我们只需把最高位
向右全部变成1,然后再加1变成10000xxxx的形式。就得到了不小于cap
的最近2的次幂.

00000000 00000000 00000000 00000100  n
00000000 00000000 00000000 00000010  n>>>1   n|n:00000000 00000000 00000000 00000110
00000000 00000000 00000000 00000001  n>>>2   n|n:00000000 00000000 00000000 00000111
00000000 00000000 00000000 00000000  n>>>4   n|n:00000000 00000000 00000000 00000111
00000000 00000000 00000000 00000000  n>>>8   n|n:00000000 00000000 00000000 00000111
00000000 00000000 00000000 00000000  n>>>16  n|n:00000000 00000000 00000000 00000111
00000000 00000000 00000000 00001000  n+1

2.1初始化和扩容

Q:HashMap是什么时候进行初始化和扩容的,扩容的长度是多少,扩容是怎么进行的。
A:你可能会说,那我判断size>table.length,不就是可以进行扩容了吗?首先table数组并不是等到装满了,再进行扩容的,因为这样子会使得hash的碰撞增多,但是又不能太小就进行扩容,因为这会导致table里面的空元素增多,对table的空间进行浪费,这就使得我们要确定一个阀值对table数组进行扩容,而确定这个阀值的因素,我们就叫做 加载因子 。在hashMap中,加载因子loadFactor默认为0.75f。为什么是这个值,我总结了几点下面几点原因:
1、从区间上来说,0.5太小,会减少hash碰撞,造成空间的浪费,定义为1的话,则会造成hash碰撞增多
2、从数学角度上来说,由于长度是2的整次幂,所以*0.75一般都可以得到一个整数的结果
3、从泊松分布上来说,0.7多可以使得碰撞的元素在8以上的概率极大减小
但是为什么是0.75这个值,其实只是个经验值,和泊松分布并没有什么关系,这是java官方自己说的.

//初始化和扩容
     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){
                //对旧的数组长度和旧的预期值进行2倍的扩容
                         
                newThr = oldThr << 1; // double threshold
            }
        } else if (oldThr > 0){ // initial capacity was placed in threshold
         //进行数组的初始化
            newCap = oldThr;
        }else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;//默认的初始化长度 1<<4 16  
            //初始化预期值
           newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        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] 里面有东西(红黑树或者链表)
                    oldTab[j] = null;//清除旧的格子,对节点进行重新分布
                    if (e.next == null){//只有一个节点
                        newTab[e.hash & (newCap - 1)] = e;//用哈希函数重新计算index
                    }else if (e instanceof TreeNode){
                        //格子里面装着红黑树,对其进行拆解...
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    }else { // preserve order
                    //格子里面装着多个节点,先初始化4个节点
                    //分别是头尾高低节点
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;//下一个节点
                        do {
                            next = e.next;
                            //oldCap是2的整次幂 (因此二进制是
                            //10000....),以16为例子,结果只可能
                            //是0或者16
                            if ((e.hash & oldCap) == 0) {
                                //代表是在0-16里面
                                //所以新的数组还在里面,就不用迁移
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }else {
                                //节点的hash是可以在16之外的
                                //进行迁移
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            //hash&16判断比较小的数据
                            //位置不变
                            loTail.next = null;
                            newTab[j] = loHead;//loHead存储着hash…&16之后比较小
                            //节点集合
                        }
                        if (hiTail != null) {
                            //hash&16判断比较小的数据
                            //位置加上旧的数组长度
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;//hiHead存储着hash&16之后比较大
                            //节点集合
                        }
                    }
                }
            }
        }
        return newTab;
    }

从上面可以看出,hashMap在进行扩容的时候,会对长度进行原来2倍的扩充,并且对table[index] 元素上面的链表进行迁移,这里注意,因为table[index]元素里面的单链表,有可能会有高的hash值(超过旧的数组长度为高)和低位的hash值,那么我们就要把低位和高位分别存在一个链表集合中,这时候源码是用(e.hash & oldCap) == 0来区分的高低位置的。以16为例,二进制为0...01000,所以&它的值要么是0,要么是16,并且用loHead和hiHead存储起来。所以,扩容之后的index要么是原来的,要么是加上+旧的长度length

3、Hashmap结构图(红黑树后面再讲解)

图1.png

这图就很好的表现了hashMap的数据结构,当key的hash的值相等的时候,它采取尾接法,形成一条单链表.

4、put过程

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
/**
  @param evict 如果为false,则表处于创建模式。
  如果为true的话,就删除最久没使用的,
  这个是用在LinkedHashMap里面的
  */
  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是否为空,为空的话初始化table
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)//(n-1)&hash 想当于hash%n 随机分布到0-15
            tab[i] = newNode(hash, key, value, null);//当前的table[i]里面没有链表,初始化
        else {
            //当前table[i]存在节点
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))){
            //如果要插入的节点已经存在,且key相等,赋值给e
                e = p;
            }else if (p instanceof TreeNode){
                //如果table[i]里面已经有红黑树,则插入红黑树,并进行旋转的操作
                //如果有相同hash和相同key的话,就返回值
                //具体请看下一篇博文红黑树,里面有具体的讲解
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            }else {
                //单链表,往下查找并插入节点  尾插法
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //找到null的节点,进行插入
                        p.next = newNode(hash, key, value, null);
                        //如果这个大于等于8,则把单链表变成红黑树
                        //这里你可能有个疑问,8-1不是等于7吗?
                        //那是因为(binCount是从下标0开始的!!!!)
                        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)))){
                        //往下找,找到相同的key节点,则break结束循环
                        break;
                        }
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
            //e不为null,则说明有相同的节点,则修改value的值
                V oldValue = e.value;
                //由于put操作的时候,onlyIfAbsent传的是false
                //则旧的值终会被替换
                /**@param onlyIfAbsent 代表着是否保留旧的value
                **/
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //afterNodeAccess 是一个回调方法,回调相同key的节点
                /**@link LinkedHashMap有用到这个**/
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;//结构修改的次数加1   迭代器 failfast
        //size 加1之后判断是否超出预期的容量值
        if (++size > threshold){
            //进行扩容
            resize();
        }
        afterNodeInsertion(evict);//也是LinkedHashMap用到的.
        //根据Lru算法来删除最久没使用的
        //节点
        return null;
    }
java8 put流程图.png

从put过程可以看出,判断table是否为空,若为空,则执行resize方法初始化table数组,如果table[i = (n-1)&hash]的值为空,则初始化一个链表节点,如果不为null,则判断头节点是否和要put的节点key相等,如果不等,就判断链表是否被红黑树化,需要搜索红黑树查找该key值是否已存在;链表未被树化,且头节点key值与新增key值不同,需要遍历链表查找该key值是否存在。如果不存在,则插入,这里需要注意的是,当链表>=8的时候,则转化成红黑树
上面的判断逻辑中,不管是哪一种,只要key值已存在,则赋值给e,因此在后面判断e是否为null,若不为空,则表示已存在,根据策略决定是否覆盖原值
如果key值不存在,需要将modCount自增1,然后将size自增,判断大小是否超过阈值,是,则通过resize方法double原table数组的容量。

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

推荐阅读更多精彩内容