JDK1.8 HashMap源码分析(三)

上一篇文章分析了get()和put(),这篇接着分析put中的resize(),顺带也看一下treeifyBin()中还有一个树化条件。

一、resize()

resize()方法的作用是用来初始化或者扩容数组的,这个方法很长,其中的逻辑需要慢慢的弄清楚,核心内容就是扩容部分,要理解为什么能够使用高低位链表。

final Node<K,V>[] resize() {
    // table: 成员变量,是存放元素的数组,这里赋值给局部变量
    Node<K,V>[] oldTab = table;
    // oldCap存储数组长度,未初始化时为0
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // threshold: 成员变量,可表示两种含义
    // 1. 当未初始化,但是传入了初始容量,则表示需要初始化的大小,这里可以再去看一下构造方法
    // 2. 初始化之后表示后续数组的扩容阈值
    int oldThr = threshold;
    // newCap: 新数组长度,newThr: 新数组扩容阈值
    int newCap, newThr = 0;
    // oldCap大于0则表示应该走扩容的逻辑
    if (oldCap > 0) {
        // MAXIMUM_CAPACITY: 数组最大可达长度,一般不会这么长
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 条件1: 首先让新数组长度=旧数组长度*2,再判断是否小于最大长度,这个条件一般为true
        // 条件2: 只有当旧数组长度大于默认初始化长度16时才会让新数组的扩容阈值翻倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // -----------------------后面全部为初始化的步骤-----------------------
    // 前置条件: oldCap = 0,在这里说明数组未初始化
    // 所以oldThr > 0表示构造方法中传入了初始化大小,所以直接赋值给newCap作为新数组长度
    else if (oldThr > 0)
        newCap = oldThr;
    // 这里的条件是 oldThr = 0,表示构造方法中未传入初始化大小,数组也没有初始化
    else {
        // 使用默认数组长度16
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 初始化数组的扩容阈值 = 默认负载因子0.75 * 默认数组长度16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // newThr == 0说明上面走的是初始化逻辑并且构造方法中传入了初始化大小
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        // newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY一般为true
        // newThr在这里赋值为初始化后的扩容阈值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 新的扩容阈值赋值给成员变量threshold
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    // 创建新的数组并赋值给成员变量table
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 这里是扩容逻辑,当oldTab为null时说明是初始化,这里就可以直接跳过
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            // e用来表示当前的元素
            Node<K,V> e;
            // 首先判断该旧数组上的第一个元素是否为null
            if ((e = oldTab[j]) != null) {
                // 把旧数组这里置空,其实是为了帮助gc进行垃圾回收
                oldTab[j] = null;
                // e在这里旧代替了旧数组该位置上的第一个元素
                if (e.next == null)
                    // 当头元素之后的元素为null时,则可以直接计算出新数组对应的下标然后迁移过去
                    newTab[e.hash & (newCap - 1)] = e;
                // 前置条件: 头元素之后还有元素
                // e如果是红黑树的节点则走红黑树的扩容逻辑,这里就不细讲红黑树了
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 走到这里说明头元素之后还有元素并且是链表
                else {
                    /**
                     * 这部分逻辑要看懂首先得明白数据存放位置是如何得来的,下面我画了一张图可以看看
                     * 图中以1号桶位举例,转移后的数据只可能映射到新数组的1号桶位或者17号桶位
                     * 我们把1号桶位称为低位,17号桶位称为高位
                     * loHead: 低位链表的头元素 loTail: 低位链表的尾元素
                     * hiHead: 高位链表的头元素 hiTail: 高位链表的尾元素
                     */
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    // 指向下一个元素
                    Node<K,V> next;
                    do {
                        next = e.next;
                        /**
                         * 这里就是在计算下图中X的值
                         * 还记得前面说过数组的容量一定是2的整数次幂吗,也就是二进制中只有一个1
                         * 假设旧数组容量为16 -> 0001 0000
                         * 经过e.hash & oldCap运算之后,要么为0000 0000要么为0001 0000
                         * X=0时放入低位链表中,X=1时放入高位链表
                         * 这里可以知道数据迁移使用的是尾插法
                         */
                        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;
                        }
                      // do while一直把这个桶位的链表遍历完毕
                    } 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;
}
数据迁移

二、treeifyBin()

这个方法主要是用于转换红黑树,内部还有replacementTreeNode()和treeify(),这些都属于红黑树部分了,在HashMap中我就不展开细聊了,以后分析红黑树源码时再来看吧,这里了解一下转换红黑树的另一个条件即可。

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // MIN_TREEIFY_CAPACITY = 64,这里如果数组长度小于64则会进入resize()方法
    // 进入resize()后就会执行扩容逻辑,所以这里可以得出结论
    // 链表树化的条件是链表元素 >= 8 并且数组长度 >= 64,如果只是链表元素到达8则会扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // 树化逻辑
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            // 把链表节点替换为红黑树节点
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        // 全部转为红黑树节点之后进行树化
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

从这里可以得知结论

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

推荐阅读更多精彩内容

  • 关键属性 构造方法 无参构造 带参构造函数 选取带参构造函数二进行分析: 逻辑流程: ①传入相应的自定义的初始容量...
    wxxhfg阅读 334评论 0 1
  • 哈希表简介 在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下(后面会探讨下哈希冲突的情况...
    西界__阅读 229评论 0 1
  • hashMap是基于hash表(散列表),实现Map接口得双列集合,数据结构是--链表散列 也就是 数组+链表,k...
    蒙古code阅读 329评论 0 0
  • [toc]无论是大厂还是不知名的小公司,HashMap都是一个绕不开的话题。基本上,如果通过HashMap能聊半小...
    冬天里的懒喵阅读 572评论 0 1
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,471评论 28 53