HashMap底层源码分析

目录

  • 一、知识点
  • 二、数据结构
  • 三、resize() 扩容方法
  • 四、putVal() 添加数据方法
  • 五、remove() 删除方法
  • 六、removeTreeNode() 退化链表方法


一、知识点

加载因子:
HashMap 的默认的加载因子: 0.75,用来限定阈值(用于控制 HashMap 的饱和度)
阈值 = 加载因子 * 数组长度
默认阈值: 16 * 0.75 = 12 (默认的初始容量:16)
在数组中存储元素数量,超过阈值,会引发数组扩容

Hash算法:
不是加密算法,不可逆
常见的哈希算法:MD4 、 MD5 、SHA1、SHA2、 SHA3

hashmap 的 hash值的计算:
// 不是哈希算法,仿照哈希算法思想
// 不够充分随机,但是速度快
// 要存储 key-value 数据,对 key 计算,得到一个 int 类型的 hash 值
// 如果kay值是null, 它对应的hash值是0
// 如果kay值不是null, 取key的hashCode 异或上  hashCode向right移动16位    
// hashCode 向低位移动16位, 让高位和低位取异或运算的目的, 就是希望高位和低位充分混合, 最终都能参加到取模预算中去 
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

手动计算HashMap的hash值举例:
String str = "study";
int hash = str.hashCode();
hash = hash ^ (hash >>> 16);
// 将哈希值映射到有限的索引范围内
int index = hash % 底层数组长度;

// hashCode(用于记录对象的位置)不一样, 那么hash值一定不一样, hashCode和hash值是一一对应关系
如果一个key-value数据在数组x下标位置, 当这个数组扩容, 扩容完成之后, 这个key-value数据要重新散列, 要么还在x位置, 要么在旧数组长度+x位置
因为默认的扩容机制是2

二、数据结构

class HashMap{
    Node[] table; // HashMap的底层数组
    int size;
    float loadFactor; // 加载因子
    float DEFAULT_LOAD_FACTOR = 0.75f;	// 默认加载因子
    int threshold; // 阈值
    // ......
}

// 存储到HashMap底层Node数组中结点的类型:
// 添加key-value数据,实际上存储到对应经过计算 hash 值取模得到的下标的位置
class Node<K,V>  {
	final int hash;		// 扩容数组后,原本存在某个位置的元素可能改变位置
	final K key;
	V value;
	Node<K,V> next;    	// HashMap底层数组中是链表
}

三、resize() 扩容方法

给定数组长度点和 ArrayDeque 中的 initialCapacity 类似,只是 initialCapacity 是大于,没有等于
在构造方法里给 HashMap 一个长度, 它会把这个长度变成一个大于等于这个值的最小的2的幂值作为底层数组的长度
HashMap 的底层结构一直会保持2的幂值

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)
	        newThr = oldThr << 1; // 默认的扩容机制:2倍
	}
	else if (oldThr > 0) 
	    // 场景一:第一次添加数据扩容,阙值不为0,数组为0
	    // 比如调用构造方法 new HashMap(initCap,loadFactor); new HashMap(initCap); 等
	    newCap = oldThr;
	else { // 场景一:第一次添加数据扩容,oldTab = null,oldCap = 0,oldThr = 0  
	    // DEFAULT_INITIAL_CAPACITY = 1 << 4;
	    // newCap(新容量) =  16
	    newCap = DEFAULT_INITIAL_CAPACITY;
	    // newThr(新阈值) = 0.75 * 16 = 12
	    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
	}
	
	// 新阙值未赋值,如上面的if-else if不成立
	if (newThr == 0) {
	    float ft = (float)newCap * loadFactor;
	    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
	              (int)ft : Integer.MAX_VALUE);
	}
	
	threshold = newThr;
	Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
	table = newTab;
	
	// 上面的代码是在创建新的数组
	// 这里把旧数组数据转移到新数组中
	if (oldTab != null) {...}
	
	return newTab;
}

四、putVal() 添加数据方法

public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K, V>[] tab;
    Node<K, V> p;
    int n, i;

    // 扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

    // (n - 1) & hash : hash值和数组长度取模
    // 因为数组长度是2的幂值,所以可以用&操作
    // i 取模之后的下标
    // p 指向key-value数据经过取模之后的下标位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K, V> e; // e指向需要修改的元素
        K k;
        // 要添加的key的位置,不为空,比较key
        // 先比较hash值,再看两个key是否直接相等或者相equals
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 判断 此处是不是树
        else if (p instanceof TreeNode)
            e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
        else {
            // 这个下标位置存储的不是树,是链表
            // 初始:p指向数组的“第一个元素”
            for (int binCount = 0; ; ++binCount) {
                // 注意:这里 e = p.next,e指向链表的第二个元素(数组中的数组为第一个元素),此时 binCount == 0
                if (( e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);                                           
                    // 当链表长度超过8达到9个数据的时候会进行红黑树的转化(阈值是8)
                    // HashMap中出现红黑树的概率很低
                    if (binCount >= TREEIFY_THRESHOLD - 1) // >= 7
                        // 转化树,不一定会构造树(发生树的转化,但不一定会将链表转换成红黑树)
                        treeifyBin(tab, hash);
                    break;
                }
                // 链表中有重复元素, key相同,新值覆盖旧值
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                
                p = e;
            }
        }
        if (e != null) { 
            // 新值覆盖旧值
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // ...
    return null;
}

转化树的源码:
final void treeifyBin(Node<K,V>[] tab, int hash) {
	int n, index; Node<K,V> e;
当底层数组数为空,或者底层数组的长度 < MIN_TREEIFY_CAPACITY (初始值为64) 时,会选择扩容,而不是创建树
因为底层数组的长度是2的幂次值,< 64,即底层数组的长度最大为 320.75 的加载因子,即底层数组允许存储的数据的最大值是 32*0.75 = 24,这时即使某一个下标位置已经产生长度为9的链表, 是选择扩容, 而非转化为红黑树resize();
	if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)

注意:
如果我们使用某些特殊的类型来充当key, 要根据上面这个条件判断是否需要重写hashCode和equals方法

在这里插入图片描述


五、remove() 删除方法

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}
    
// 注:也可使用map.clear();将map中的所有元素全部删光

六、removeTreeNode() 退化链表方法

情况一:
移除元素 remove() 操作:
当红黑树(根节点/根节点的左右结点/根节点的左结点的左结点)这四个结点有任意一个不存在, 就要由红黑树转化为链表  
在final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,boolean movable) {...}中
关键代码:            
if (root == null || root.right == null ||
    (rl = root.left) == null || rl.left == null) {
    tab[index] = first.untreeify(map);  // too small
    return;
}


情形二:
底层数组的扩容resize()方法,导致树拆成两部分,树中的结点过少,转化成链表
判断节点类型,如果是链表,则将链表拆分,如果是TreeNode,则执行TreeNode的split方法分割红黑树
if (lc <= UNTREEIFY_THRESHOLD) // 低位;UNTREEIFY_THRESHOLD = 6
	tab[index] = loHead.untreeify(map);
if (hc <= UNTREEIFY_THRESHOLD) // 高位
	tab[index + bit] = hiHead.untreeify(map);

为什么链表转化为红黑树阈值是8, 红黑树转化为链表阈值是6 ?
担心如果阈值设为一样, 添加和删除可能导致红黑树和链表间的反复转化

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/710001.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

适合小白学习的项目1906java Web智慧食堂管理系统idea开发mysql数据库web结构java编程计算机网页源码servlet项目

一、源码特点 java Web智慧食堂管理系统是一套完善的信息管理系统&#xff0c;结合java 开发技术和bootstrap完成本系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 前段主要技术 bootstra…

uni-app 小程序:显示图片并且点击图片展示大图

效果如图所示&#xff1a; 在页面显示一张图片&#xff0c;然后点击该张图片后显示大图。点击大图就可以关闭大图。 实现的主要代码如下&#xff1a; <image :src"imgpath" mode"aspectFill" click"imgPreview(imgArr)"></image> 其…

LeetCode | 171.Excel表列序号

这道题涉及到字符串和进制转换&#xff0c;首先我们先创建一个A-Z到1-26的map映射&#xff0c;方便我们后续遍历字符串转换&#xff0c;然后对字符串从后往前遍历&#xff0c;依次加上对应权重&#xff0c;注意越往前的权重越大&#xff0c;要记得对应乘上26的对应方数 class …

Visual Studio Code 的安装教程和配置C语言环境插件推荐

目录 1.vscode简介2.下载安装vs code3.VSCode基础配置VSCode界面简介VSCode设置中文界面VSCode个性化设置VSCode常用设置基本编辑快捷键VSCode常用快捷键 4.下载安装MinGW5.设置vscode里的环境6.插件推荐7.vscode官方文档 1.vscode简介 VSCode是微软出的一款轻量级编辑器&…

Xilinx SDK操作步骤详细介绍

在vivado设计完成后&#xff0c;下一步就是软件设计&#xff0c;与vivado相配套的设计软件是xilinx SDK(software developement kit&#xff09;&#xff0c;其操作流程如下&#xff1a; Vivado软件的bitstream文件成功生成后&#xff0c;点击File——Export——Export Hardwa…

IQ Products—Hemoglobin antibodies for flow cytometry

血红蛋白&#xff08;Hemoglobin&#xff09;英文缩写为HGB或Hb。血红蛋白是红细胞内运输氧的特殊蛋白质&#xff0c;是使血液呈红色的蛋白&#xff0c;由珠蛋白和血红素组成&#xff0c;其珠蛋白部分是由两对不同的珠蛋白链&#xff08;α链和β链&#xff09;组成的四聚体。血…

异或运算在面试题中的应用

异或运算 是 涉及到数据位运算时常见的处理方式。如何进行异或运算&#xff1f;在对应位上&#xff0c;相同为0&#xff0c;不同1&#xff0c;但其实两个数据异或运算就是进行无进位加法。 例如&#xff1a; int a 7, b 6, a ^b ? 算法1: 相同为0&#xff0c;不同为1 a …

边界内聚和耦合

内聚 功能内聚 功能内聚是软件工程中一个重要的概念&#xff0c;它描述了一个模块内部各个元素之间的紧密程度。一个具有高功能内聚的模块意味着其内部的各个组件都共同完成一个具体的、明确的功能&#xff0c;并且这些组件之间的联系不是偶然的&#xff0c;而是因为它们共同服…

sslscan一键检测服务器协议信息和加密算法(KALI工具系列二十四)

目录 1、KALI LINUX 简介 2、sslscan工具简介 3、信息收集 3.1 目标主机IP&#xff08;服务器&#xff09; 3.2 KALI的IP 4、操作示例 4.1 扫描主机 4.2 指定端口 4.3 输出详细信息 4.4 打印输出信息 4.4 检测协议 5、总结 1、KALI LINUX 简介 Kali Linux 是一个…

在下游市场需求带动下 我国聚天门冬氨酸脂防腐涂料市场规模不断扩大

在下游市场需求带动下 我国聚天门冬氨酸脂防腐涂料市场规模不断扩大 聚天门冬氨酸酯防腐涂料又称为天冬聚脲防腐涂料&#xff0c;是以聚天门冬氨酸酯作为主体树脂、脂肪族异氰酸酯为固化剂而形成的一种防腐涂料。与其他类型的防腐涂料相比&#xff0c;聚天门冬氨酸酯防腐涂料具…

IO系列(十) -TCP 滑动窗口原理介绍(上)

一、摘要 之前在上分享网络编程知识文章的时候&#xff0c;有网友写下一条留言&#xff1a;“可以写写一篇关于 TCP 滑动窗口原理的文章吗&#xff1f;”。 当时没有立即回复&#xff0c;经过查询多方资料&#xff0c;发现这个 TCP 真的非常非常的复杂&#xff0c;就像一个清…

《TCP/IP网络编程》(第十五章)套接字和标准I/O

之前数据通信时&#xff0c;使用的是read&write函数以及其他各种I/O函数&#xff0c;本章将使用标准I/O函数&#xff0c;例如C语言的fopen、fgetc、fputs等等&#xff1b;C语言的cout、cin等等 1.使用标准I/O函数的优点 ①跨平台兼容性&#xff1a; 标准I/O函数通常是跨平…

依赖自动装配

黑马程序员SSM框架 文章目录 1、依赖自动装配2、依赖自动装配的特征 1、依赖自动装配 IoC容器根据bean所依赖的资源在容器中自动查找并注入到bean中的过程称为自动装配自动装配方式 按类型&#xff08;常用&#xff09;按名称按构造方法不启用自动装配 配置中使用bean标签auto…

68. UE5 RPG 优化敌人角色的表现效果

我们现在已经有了四个敌人角色&#xff0c;接下来&#xff0c;处理一下在战斗中遇到的问题。 处理角色死亡后还会攻击的问题 因为我们有角色溶解的效果&#xff0c;角色在死亡以后的5秒钟才会被销毁掉。所以在这五秒钟之内&#xff0c;角色其实还是会攻击。主要时因为AI行为树…

flash介绍(zynq篇)

简介&#xff1a;Flash存储器&#xff08;又称闪存&#xff09;是一种非易失性存储器. 页是读写的基本操作单位。&#xff08;页写前需要进行擦除操作&#xff08;全部为1&#xff09;&#xff0c;写操作是实现1→0操作&#xff09; 注意&#xff1a;zynq中有板载flash控制器的…

服务部署:Linux系统部署C# .NET项目

1. 安装 .NET SDK 首先&#xff0c;你需要在你的 Linux 系统上安装 .NET SDK。 Ubuntu系统&#xff1a; 下载 Microsoft 包配置文件 wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb 这个命令使用 wge…

大模型Prompt-Tuning技术入门

Prompt-Tuning方法 1 NLP任务四种范式 目前学术界一般将NLP任务的发展分为四个阶段&#xff0c;即NLP四范式&#xff1a; 第一范式&#xff1a;基于「传统机器学习模型」的范式&#xff0c;如TF-IDF特征朴素贝叶斯等机器算法&#xff1b;第二范式&#xff1a;基于「深度学习模…

java打印99乘法表

public class NineNineMulTable{public static void main(String[] args){for(int i 1; i < 9; i ){for(int j 1; j < i; j ){System.out.print(j " * " i " " i * j "\t");//再次先输出j在输出i是打印出来是1*2&#xff0c;2*2}S…

网络安全技术实验六 入侵检测技术实践

一、实验目的和要求 理解基于网络的入侵检测系统的基本原理&#xff0c;掌握snort IDS工作机理&#xff1b; 学习应用snort三种方式工作&#xff1b;熟练编写snort规则&#xff1b; 完成snort数据包记录、日志查看、字符串匹配、ARP欺骗攻击检测、端口扫描工具检测等功能。 …

【计算机视觉】人脸算法之图像处理基础知识(二)

图像处理基础知识&#xff08;二&#xff09; 1.图像的颜色空间转换 我们常见的图像通常由R&#xff08;红色&#xff09;、G&#xff08;绿色&#xff09;、B&#xff08;蓝色&#xff09;组成。但是在很多时候我们会将彩色图像转换成灰度图像进行处理。此时会用到cv2.cvtCo…