Java源码系列4——HashMap扩容时究竟对链表和红黑树做了什么?
我们知道 HashMap 的底层是由数组,链表,红黑树组成的,在 HashMap 做扩容操作时,除了把数组容量扩大为原来的两倍外,还会对所有元素重新计算 hash 值,因为长度扩大以后,hash值也随之改变。
如果是简单的 Node 对象,只需要重新计算下标放进去就可以了,如果是链表和红黑树,那么操作就会比较复杂,下面我们就来看下,JDK1.8 下的 HashMap 在扩容时对链表和红黑树做了哪些优化?
rehash 时,链表怎么处理?
假设一个 HashMap 原本 bucket 大小为 16。下标 3 这个位置上的 19, 3, 35 由于索引冲突组成链表。

当 HashMap 由 16 扩容到 32 时,19, 3, 35 重新 hash 之后拆成两条链表。

查看 JDK1.8 HashMap 的源码,我们可以看到关于链表的优化操作如下:
// 把原有链表拆成两个链表
// 链表1存放在低位(原索引位置)
Node<K,V> loHead = null, loTail = null;
// 链表2存放在高位(原索引 + 旧数组长度)
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
    next = e.next;
    // 链表1
    if ((e.hash & oldCap) == 0) {
        if (loTail == null)
            loHead = e;
        else
            loTail.next = e;
        loTail = e;
    }
    // 链表2
    else {
        if (hiTail == null)
            hiHead = e;
        else
            hiTail.next = e;
        hiTail = e;
    }
} while ((e = next) != null);
// 链表1存放于原索引位置
if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;
}
// 链表2存放原索引加上旧数组长度的偏移量
if (hiTail != null) {
    hiTail.next = null;
    newTab[j + oldCap] = hiHead;
}
正常我们是把所有元素都重新计算一下下标值,再决定放入哪个桶,JDK1.8 优化成直接把链表拆成高位和低位两条,通过位运算来决定放在原索引处或者原索引加原数组长度的偏移量处。我们通过位运算来分析下。
先回顾一下原 hash 的求余过程:

再看一下 rehash 时,判断时做的位操作,也就是这句 e.hash & oldCap:

再看下扩容后的实际求余过程:

这波操作是不是很666,为什么 2 的整数幂 - 1可以作 & 操作可以代替求余计算,因为 2 的整数幂 - 1 的二进制比较特殊,就是一串 11111,与这串数字 1 作 & 操作,结果就是保留下原数字的低位,去掉原数字的高位,达到求余的效果。2 的整数幂的二进制也比较特殊,就是一个 1 后面跟上一串 0。
HashMap 的扩容都是扩大为原来大小的两倍,从二进制上看就是给这串数字加个 0,比如 16 -> 32 = 10000 -> 100000,那么他的 n - 1 就是 15 -> 32 = 1111 -> 11111。也就是多了一位,所以扩容后的下标可以从原有的下标推算出来。差异就在于上图我标红的地方,如果标红处是 0,那么扩容后再求余结果不变,如果标红处是 1,那么扩容后再求余就为原索引 + 原偏移量。如何判断标红处是 0 还是 1,就是把 e.hash & oldCap。
rehash 时,红黑树怎么处理?
// 红黑树转链表阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 扩容操作
final Node<K,V>[] resize() {
    // ....
    else if (e instanceof TreeNode)
       ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
    // ...
}
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;
    // Relink into lo and hi lists, preserving order
    // 和链表同样的套路,分成高位和低位
    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
    int lc = 0, hc = 0;
    /**
      * TreeNode 是间接继承于 Node,保留了 next,可以像链表一样遍历
      * 这里的操作和链表的一毛一样
      */
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        e.next = null;
        // bit 就是 oldCap
        if ((e.hash & bit) == 0) {
            if ((e.prev = loTail) == null)
                loHead = e;
            else
            // 尾插
                loTail.next = e;
            loTail = e;
            ++lc;
        }
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }
    // 树化低位链表
    if (loHead != null) {
        // 如果 loHead 不为空,且链表长度小于等于 6,则将红黑树转成链表
        if (lc <= UNTREEIFY_THRESHOLD)
            tab[index] = loHead.untreeify(map);
        else {
            /**
              * hiHead == null 时,表明扩容后,
              * 所有节点仍在原位置,树结构不变,无需重新树化
              */
            tab[index] = loHead;
            if (hiHead != null) // (else is already treeified)
                loHead.treeify(tab);
        }
    }
    // 树化高位链表,逻辑与上面一致
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}
从源码可以看出,红黑树的拆分和链表的逻辑基本一致,不同的地方在于,重新映射后,会将红黑树拆分成两条链表,根据链表的长度,判断需不需要把链表重新进行树化。
摘自
https://www.jianshu.com/p/87d2ef48e645
Java源码系列4——HashMap扩容时究竟对链表和红黑树做了什么?的更多相关文章
- Java源码系列2——HashMap
		
HashMap 的源码很多也很复杂,本文只是摘取简单常用的部分代码进行分析.能力有限,欢迎指正. HASH 值的计算 前置知识--位运算 按位异或操作符^:1^1=0, 0^0=0, 1^0=0, 值 ...
 - Java源码系列1——ArrayList
		
本文简单介绍了 ArrayList,并对扩容,添加,删除操作的源代码做分析.能力有限,欢迎指正. ArrayList是什么? ArrayList 就是数组列表,主要用来装载数据.底层实现是数组 Obj ...
 - 【java集合框架源码剖析系列】java源码剖析之HashMap
		
前言:之所以打算写java集合框架源码剖析系列博客是因为自己反思了一下阿里内推一面的失败(估计没过,因为写此博客已距阿里巴巴一面一个星期),当时面试完之后感觉自己回答的挺好的,而且据面试官最后说的这几 ...
 - Java源码解读(一)——HashMap
		
HashMap作为常用的一种数据结构,阅读源码去了解其底层的实现是十分有必要的.在这里也分享自己阅读源码遇到的困难以及自己的思考. HashMap的源码介绍已经有许许多多的博客,这里只记录了一些我看源 ...
 - Java 源码刨析 - HashMap 底层实现原理是什么?JDK8 做了哪些优化?
		
[基本结构] 在 JDK 1.7 中 HashMap 是以数组加链表的形式组成的: JDK 1.8 之后新增了红黑树的组成结构,当链表大于 8 并且容量大于 64 时,链表结构会转换成红黑树结构,它的 ...
 - 大白话Vue源码系列(05):运行时鸟瞰图
		
阅读目录 Vue 实例的生命周期 实例创建 响应的数据绑定 挂载到 DOM 节点 结论 研究 runtime 一边 Vue 一边源码 初看 Vue 是 Vue 源码是源码 再看 Vue 不是 Vue ...
 - [java源码解析]对HashMap源码的分析(二)
		
上文我们讲了HashMap那骚骚的逻辑结构,这一篇我们来吹吹它的实现思想,也就是算法层面.有兴趣看下或者回顾上一篇HashMap逻辑层面的,可以看下HashMap源码解析(一).使用了哈希表得“拉链法 ...
 - [java源码解析]对HashMap源码的分析(一)
		
最近有空的时候研究了下HashMap的源码,平时我用HashMap主要拿来当业务数据整理后的容器,一直觉得它比较灵活和好用, 这样 的便利性跟它的组成结构有很大的关系. 直接开门见山,先简要说明一下H ...
 - 【java集合框架源码剖析系列】java源码剖析之HashSet
		
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本.本博客将从源码角度带领大家学习关于HashSet的知识. 一HashSet的定义: public class HashSet&l ...
 
随机推荐
- charles功能(五)屏蔽web网页的抓包信息(proxy)
			
应用场景:屏蔽web网页的抓包信息 proxy-->windows proxy(前面没有对勾,就不会抓到 PC浏览器的包) proxy-->macOS proxy(mac电脑) 最终效果
 - Java面试专题-多线程篇(2)- 锁和线程池
 - PyQt(Python+Qt)学习随笔:QAbstractItemView的dragEnabled和dragDropMode属性的关系
			
老猿Python博文目录 老猿Python博客地址 在<PyQt(Python+Qt)学习随笔:QAbstractItemView的dragEnabled属性的困惑>中,老猿觉得dragE ...
 - Leetcode学习笔记(3)
			
题目1 ID88 给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 num1 成为一个有序数组. 说明: 初始化 nums1 和 nums2 的元素数量 ...
 - Python中错误之 TypeError: object() takes no parameters、TypeError: this constructor takes no arguments
			
TypeError: object() takes no parameters TypeError: this constructor takes no arguments 如下是学习python类时 ...
 - react-admin-plus 正式开源, 欢迎star
			
简介 基于react.ant-ui.typescript的前端微服务框架.欢迎star. 在线地址 在线demo 项目介绍 沉淀了几个月的时间,这款框架终于正式的和大家见面了! 先说一下我做这 ...
 - STL——容器(deque) deque 的大小
			
1. deque 的大小 deque.size(); //返回容器中元素的个数 1 #include <iostream> 2 #include <dequ ...
 - 一、安装LoadRunner12
			
今天接到任务最近要进行性能测试(刚开始搞自动化,有要搞性能测试,领导嫌我不忙吧),之前做接口测试用过Jmeter,也可以使用Jmeter做性能测试,但公司要求用LoadRunner,开始学习性能测试和 ...
 - linux下postgresql安装
			
1.下载地址 https://www.enterprisedb.com/download-postgresql-binaries 2.创建用户并修改密码 [root@node01 ~]# userad ...
 - SpringBoot瘦身部署(15.9 MB - 92.3 KB)
			
1. 简介 SpringBoot项目部署虽然简单,但是经常因为修改了少量代码而需要重新打包上传服务器重新部署,而公网服务器的网速受限,可能整个项目的代码文件仅仅只有1-2MB甚至更少,但是需要上传 ...