HashMap底层源码分析

2023-11-01

HashMap:

        HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。

       HashMap是非线程安全的,实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。

        HashMap 是无序的,即不会记录插入的顺序。

        之前jdk1.7的存储结构是数组+链表,到了jdk1.8变成了数组+链表+红黑树。


接口与继承图:

HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口。 


底层数据结构:

jdk1.7存储结构图:

         jdk1.7中,首先是把元素放在一个个数组里面,后来存放的数据越来越多,后来出现了链表,对于数组中的每一个元素,都可以有一条链表来存储元素。这就是有名的“拉链式”存储方法。

jdk8存储结构图:

         jdk1.8我们会发现优化的部分就是把链表结构变成了红黑树。原来jdk1.7的优点是增删效率高,于是在jdk1.8的时候,不仅仅增删效率高,而且查找效率也提升了。当然不是说变成了红黑树效率就一定提高了,只有在链表的长度不小于8,而且数组的长度不小于64的时候才会将链表转化为红黑树。


成员变量:

    // 序列化id
    private static final long serialVersionUID = 362498820763181265L;
    // HashMap容器初始值  1 << 4 = 16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    // HashMap容量极限     1 << 30 = 1073741824
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 负载因子 0.75   
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 当节点数大于8时 数组长度大于64时  会转为红黑树存储
    static final int TREEIFY_THRESHOLD = 8;
    // 当节点数小于6时会转为单向链表存储
    static final int UNTREEIFY_THRESHOLD = 6;
    // 当节点数大于8时 数组长度大于64时  红黑树树化
    static final int MIN_TREEIFY_CAPACITY = 64;
    // Node是Map.Entry接口的实现类
    // 在此存储数据的Node数组容量是2次幂
    // 每一个Node本质都是一个单向链表
    transient Node<K,V>[] table;
    // HashMap大小,它代表HashMap保存的键值对的多少
    transient int size;
    //HashMap被改变的次数
    transient int modCount;
    // 下一次HashMap扩容的阈值
    int threshold;
    // 存储负载因子的常量
    final float loadFactor;

构造方法:

     /*
        initialCapacity 初始容量
        loadFactor 负载因子  表示一个散列表的空间的使用程度
    */
     public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)    // 初始容量<0 抛异常
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)   // 初始容量 > 最大容量
            initialCapacity = MAXIMUM_CAPACITY;    初始容量 等于 最大容量
        if (loadFactor <= 0 || Float.isNaN(loadFactor))  // 负载因子  小于等于0 或者 不是个数 抛异常   
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;  // 将负载因子赋给该对象
        this.threshold = tableSizeFor(initialCapacity);   // 下次达到这个值 就扩容
    }


    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);  // 负载因子是0.75
    }

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // 默认初始容量大小是 16
    }

    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;   // 负载因子是0.75
        putMapEntries(m, false);    //此构造方法主要实现了Map.putAll()
    }

put方法:

    // k-v值存
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    

    /*
        hash  计算key的hashcode值
        key   key值
        value  value值
        onlyIfAbsent  如果hash冲突时,新值是否替换旧值 如果为真 不能改变已存在的值
        evict  如果为假  这个表格在创建状态
    */
    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长度不可为0,否则将从resize函数中获取
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 通过取模运算(前文详细写道),来获得寻址结果
        / /也就是传入的key-value键值对在数组中的存储位置   
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 如果为null,说明此处还没有存储元素,将key-value包装成Node设置在i处
            tab[i] = newNode(hash, key, value, null);
        else {  // else说明寻址结果i已经存储的有元素了,哈希冲突了
            Node<K,V> e; K k;  // 两个临时变量,node和key

            // 要插入的key和原位置的key一样  这里将p赋给e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 此时说明p已经树化,调用红黑树的方法添加到指定位置
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else { // //走到这里说明,hash寻址冲突了,并且和寻址结果i处的key不同,
                // 也不是树,说明此时要在链表上操作了 找到链表的尾节点,插入新节点
                for (int binCount = 0; ; ++binCount) {  // 向node单向链表中添加数据
                    if ((e = p.next) == null) {   // 找到链表最后一个
                        p.next = newNode(hash, key, value, null); // 赋值
                        if (binCount >= TREEIFY_THRESHOLD - 1) // 节点数大于8
                            treeifyBin(tab, hash);  // 转换为红黑树
                        break;
                    }
                   //此时在链表找尾节点时,发现了和新插入节点完全一致的key,
                   // 所以记录,跳出,后文替换
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;  // p记录下一个节点
                }
            }
            if (e != null) {  //替换操作,如果e!=null,说明e记录了冲突的节点
	            //记录老值,用于返回
                V oldValue = e.value;
                // 开头传入的参数flase,说明冲突时会进行替换操作
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                 // 具体实现在LinkedHashMap,此处不在详解
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;  // 具体实现在LinkedHashMap,此处不在详解
        if (++size > threshold)  // 判断是否需要扩容
            resize();
        // 在插入后对 head 进行处理,
        // 如果满足条件(即 evict 为 true,存在至少一个元素,且允许删除第一个元素)
        // 则删除 head 所在的节点
        afterNodeInsertion(evict);
        return null;
    }

        JDK1.8之前,插入数据时,在链表中用头插法,但在多线程中可能会产生死链,所以在JDK1.8中在链表中使用尾插法来规避这个问题。 

hash方法

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

键值对在哈希表中的位置(数组index)     i = (n - 1) & hash

resize方法  扩容

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;  // table赋予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;  // 下次扩容大小 Integer.MAX_VALUE
                return oldTab;   // 返回老数组
            }
            // 若新表大小(oldCap*2)小于数组最大值 并且老值大于等于数组初始化大小
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 2 * oldThr 当作新表阈值
        }
        else if (oldThr > 0) // 如果老表下次扩容大小大于0
            newCap = oldThr;   // 新表长度 = 旧表阈值
        else {               // 零初始阈值表示使用默认值
            newCap = DEFAULT_INITIAL_CAPACITY; // 信标长度 = 16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 新表阈值等于 默认 0.75 * 16
        }
        if (newThr == 0) { // 如果新表阈值 = 0
            float ft = (float)newCap * loadFactor;  // 新表长度 * 0.75 默认
            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;  // 将当前表赋给table
        if (oldTab != null) {  // 如果oldTab中有值  循环遍历出
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) { // 获取老表第j个元素  赋给e
                    oldTab[j] = null;   // 并将老表中的元素数据置Null
                    if (e.next == null) // 如果e后面没有节点了
                        newTab[e.hash & (newCap - 1)] = e; // 直接将e赋给新表
                    else if (e instanceof TreeNode)  // 若e是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循环 获取新旧索引的节点
                        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;
    }

        门限值等于(负载因子)x(容量),如果构建 HashMap 的时候没有指定它们,那么就是依据相应的默认常量值。

       门限通常是以倍数进行调整 (newThr = oldThr << 1),我前面提到,根据 putVal 中的逻辑,当元素个数超过门限大小时,则调整 Map 大小。

putAll方法

    
    public void putAll(Map<? extends K, ? extends V> m) {
        putMapEntries(m, true);
    }



    // evict  表是否在创建模式,如果为false,则表是在创建模式。
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();  // 算出传进来表的大小
        if (s > 0) {
            if (table == null) { // 表等于null 说明是拷贝构造函数来调用的putMapEntries或者构造后还没放过任何元素
            // 先不考虑容量必须为2的幂,那么下面括号里会算出来一个容量,使得size刚好不大于阈值。
            // 但这样会算出小数来,但作为容量就必须向上取整,所以这里要加1
                float ft = ((float)s / loadFactor) + 1.0F;  
               // 如果小于最大容量,就进行截断;否则就赋值为最大容量
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                //  虽然上面一顿操作猛如虎,但只有在算出来的容量t > 当前暂存的容量(容量可能会暂放到阈值上的)时,才会用t计算出新容量,再暂时放到阈值上
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            // 说明table已经初始化过了;判断传入map的size是否大于当前map的threshold,如果是,必须要resize
            // 这种情况属于预先扩大容量,再put元素
            else if (s > threshold)
                resize();
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { // 赋值 
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

clone方法

    public Object clone() {
        HashMap<K,V> result;   // 构建一个HashMap
        try {
            result = (HashMap<K,V>)super.clone();  // 父类的复制 此时result里的值都是本hashmap的值,引用地址也一样
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        } 
        result.reinitialize();    // 将result 恢复至初始状态
        result.putMapEntries(this, false);  // 将本hashMap的值传入result中
        return result;  // 返回值  属于浅拷贝
    }


    protected Object clone() throws CloneNotSupportedException {
        AbstractMap<?,?> result = (AbstractMap<?,?>)super.clone();
        result.keySet = null;  // 清空keySet   这两个值是作为视图来使用的
        result.values = null;  // 清空values
        return result;
    }

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

HashMap底层源码分析 的相关文章

随机推荐

  • 初始化int类型data1[ ]={1,3,5,7,9,11,13,15,17,19,2,4,6,8,10,12,14,16,18,20}先使用任意一种算法对其排序提示用户输入一个数字,再折半查找

    初始化int类型data1 1 3 5 7 9 11 13 15 17 19 2 4 6 8 10 12 14 16 18 20 先使用任意一种算法对其排序提示用户输入一个数字 应用折半查找函数模板找出它的位置 include using
  • 一些计算机方面的感悟

    1 架构设计的本质是深入理解业务场景之后用工程经验做出最佳权衡 2 计算机解决问题其实没有任何奇技淫巧 它唯一的解决办法就是穷举 穷举所有可能性 算法设计无非就是先思考 如何穷举 然后再追求 如何聪明地穷举
  • Android项目针对libs(armeabi,armeabi-v7a,x86)进行平台兼容

    1 Android设备如何加载 so文件 不同CPU架构的Android手机加载时会在libs下找自己对应的目录 从对应的目录下寻找需要的 so文件 如果没有对应的目录 就会去armeabi下去寻找 如果已经有对应的目录 但是如果没有找到对
  • , trailing comma 逗号的问题

    PHP 数组元素最好加上逗号 因为可以方便其他人添加内容JAVASCRIPT 其实也应该加上逗号的 但可惜IE9以下不认 所以 可以不加逗号JSON JSON hates trailing commasPYTHON 希望尾部元素有逗号 转载
  • _mm_pause

    翻译自Intel指令 PAUSE指令提升了自旋等待循环 spin wait loop 的性能 当执行一个循环等待时 Intel P4或Intel Xeon处理器会因为检测到一个可能的内存顺序违规 memory order violation
  • 1.2冯•诺依曼模型

    文章目录 1 2 1 4个子系统 1 2 2 存储程序概念 1 2 3 指令的顺序执行 前一节中讲到的基于图灵机所建造的计算机是在存储器中存储数据 在1944 1945年期间 冯 诺依曼指出 程序和数据在逻辑上是相同的 因此程序也能存储在计
  • Mariadb主从复制之MHA配置

    Mariadb主从复制之MHA配置 一 环境介绍 1 主从复制及半同步复制配置链接 2 IP规划 二 检查一主两从数据库状态 1 主库状态 2 从库状态 三 MHA高可用介绍 1 MAH介绍 2 MAH作用 四 MHA基本环境配置 1 所有
  • linux线程使用

    概念 1 PCB Process Control Block 进程管理块 系统中存放进程的管理和控制信息的数据结构体 每一个进程均有一个PCB 在创建进程时建立 直到进程撤销而撤销 2 程序段 是进程中能被进程调度程序在CPU上执行的程序代
  • STL空间配置

    SGI STL有两级配置器 第一级配置器的allocate 和 realloc 都是在调用malloc 和 realloc 不成功后 改调用oom malloc 和 oom realloc 后两者都有内循环 不断调用 内存不足处理例程 期望
  • Unity3D中读取CSV文件

    转自 https www cnblogs com lingLuoChengMi p 9990488 html 本人对原文进行了整理 适当加上注释和小部分修改 不过大部分代码也是转载 说明 1 写入一个单元格时 如果含有逗号 则需要将整个字段
  • NVIDIA可编程推理加速器TensorRT学习笔记(三)——加速推理

    文章目录 简单张量RT示例 将预训练的图像分割 PyTorch 模型转换为 ONNX 将 ONNX 模型导入 TensorRT 生成引擎并执行推理 对输入进行批处理 分析应用程序 优化您的应用程序 使用混合精度计算 更改工作区大小 重用 T
  • 主成分分析二级指标权重_权重赋值之“主成分分析法”

    主成分分析 Principal Component Analysis PCA 最早是由K 皮尔森 Karl Pearson 对非随机变量引入的一种统计方法 尔后H 霍特林将此方法推广到随机向量的情形 主成分是指通过正交变换将一组可能存在相关
  • 阿里云物联网Iot设备上下线状态数据流转的设置

    要想通过物联网平台实现远程监控设备 那么就要建立监控端设备 比如手机 和被监控端设备的数据交互 在阿里云物联网平台完成这个交互功能的方法就是建立两个设备之间的数据流转 对于设备要流转的物模型数据 阿里云网站上已经有详细的示例介绍 但是对于设
  • 最大上升序列Super Jumping! Jumping! Jumping!

    多组输入 第一个数代表有多少个数据 输入0结束 Sample Input 3 1 3 2 4 1 2 3 4 4 3 3 2 1 0 Sample Output 4 10 3 1到3最大 1到2到3到4最大 直接到三最大 include
  • 尚硅谷 Vue 2.0 + Vue 3.0 入门到精通教程学习笔记 (二)

    第二章 Vue 组件化编程 2 1 模块与组件 模块化与组件化 2 1 1 模块 1 理解 向外提供特定功能的 js 程序 一般就是一个 js 文件 2 为什么 js 文件很多很复杂 3 作用 复用 js 简化 js 的编写 提高 js 运
  • Qt 无边框、透明、可移动、的个性窗体

    原文地址 转载 Qt 无边框 透明 可移动 的个性窗体案例详解 作者 风贝 很多朋友都问透明的效果怎么做 为什么自己做的无边框窗体不可移动 一个个回答的很累 干脆写出来分享下好了 我只用代码说话 工程的main cpp int main i
  • python菜单栏_「每日一练」Python实现下拉和弹出式菜单

    用Python就一定要用到界面操作 有一个好的用户界面 才会有好的用户体验 下边就开始创建我们的主窗口 并实现下拉和弹出式菜单 案例 创建主窗口 并实现下拉和弹出式菜单 先上代码 运行效果 题目详述 第一行 import tkinter a
  • Jupyter notebook显示连接失败、服务器正忙

    pip install tornado 4 5 成功
  • # AutoLeaders控制组—51单片机学习笔记(LED控制、独立按键、数码管)

    51单片机 1 单片机基础 1 1 内部构成 CPU RAM ROM 定时器 中断系统 通讯接口等 相当于袖珍版计算机 一个芯片能构成完整的计算机系统 1 2 51单片机 公司 STC公司 位数 8位 RAM 512字节 第二天丢失 相当于
  • HashMap底层源码分析

    HashMap HashMap 是一个散列表 它存储的内容是键值对 key value 映射 HashMap是非线程安全的 实现了 Map 接口 根据键的 HashCode 值存储数据 具有很快的访问速度 最多允许一条记录的键为 null