HashMap的扩容机制

HashMap简介

HashMap在底层数据结构上采用了数组+链表+红黑树,通过散列映射来存储键值对数据因为在查询上使用散列码(通过键生成一个数字作为数组下标,这个数字就是hash code)所以在查询上的访问速度比较快,HashMap最多允许一对键值对的Key为Null,允许多对键值对的value为Null。它是非线程安全的。在排序上面是无序的。

HashMap的主要成员变量

transient Node<K,V>[] table:这是一个Node类型的数组(也有称作Hash桶),可以从下面源码中看到静态内部类Node在这边可以看做就是一个节点,多个Node节点构成链表,当链表长度大于8的时候转换为红黑树。

transient int size:表示当前HashMap包含的键值对数量
transient int modCount:表示当前HashMap修改次数
int threshold:表示当前HashMap能够承受的最多的键值对数量,一旦超过这个数量HashMap就会进行扩容
final float loadFactor:负载因子,用于扩容
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4:默认的table初始容量
static final float DEFAULT_LOAD_FACTOR = 0.75f:默认的负载因子
static final int TREEIFY_THRESHOLD = 8: 链表长度大于等于该参数转红黑树
static final int UNTREEIFY_THRESHOLD = 6: 当树的节点数小于等于该参数转成链表
介绍完了重要的几个参数后我们来看看HashMap的构造参数。

HashMap的构造方法有四种

public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);}public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);}

前面三个构造器的区别都是在于指定初始容量以及负载因子,如果你选择默认的构造器那么在创建的时候不会指定threshold的值,而第二个以及第三个构造器在一开始的时候就会根据下面的这个方法来确认threshold值,可以看到下面用到了移位算法(有关内容可以查看博文:Java移位操作符以及按位操作符),最后一个构造器很显然就是把另一个Map的值映射到当前新的Map中这边不再赘述。

/*** Returns a power of two size for the given target capacity.* 返回距离指定参数最近的2的整数次幂,例如7->8, 8->8, 9->16, 17->32* 保证为2的幂次方*/static final int tableSizeFor(int cap) {int n = cap - 1;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;}

这边先提下负载因子(loadFactor),源码中有个公式为threshold = loadFactor * 容量。HashMap和HashSet都允许你指定负载因子的构造器,表示当负载情况达到负载因子水平的时候,容器会自动扩容,HashMap默认使用的负载因子值为0.75f(当容量达到四分之三进行再散列(扩容))。当负载因子越大的时候能够容纳的键值对就越多但是查找的代价也会越高。所以如果你知道将要在HashMap中存储多少数据,那么你可以创建一个具有恰当大小的初始容量这可以减少扩容时候的开销。但是大多数情况下0.75在时间跟空间代价上达到了平衡所以不建议修改。

下面将根据默认的构造为出发点,从初始化一个HashMap到使用Get,Put方法进行一些源码解析。

put(K key, V value)

在使用默认构造器初始化一个HashMap对象的时候,首次Put键值对的时候会先计算对应Key的hash值通过hash值来确定存放的地址。

紧接着调用了putVal方法,在刚刚初始化之后的table值为null因此程序会进入到resize()方法中。而resize方法就是用来进行扩容的(稍后提到)。扩容后得到了一个table的节点(Node)数组,接着根据传入的hash值去获得一个对应节点p并去判断是否为空,是的话就存入一个新的节点(Node)。反之如果当前存放的位置已经有值了就会进入到else中去。接着根据前面得到的节点p的hash值以及key跟传入的hash值以及参数进行比较,如果一样则替覆盖。如果存在Hash碰撞就会以链表的形式保存,把当前传进来的参数生成一个新的节点保存在链表的尾部(JDK1.7保存在首部)。而如果链表的长度大于8那么就会以红黑树的形式进行保存。

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) //首次初始化的时候table为nulln = (tab = resize()).length; //对HashMap进行扩容if ((p = tab[i = (n - 1) & hash]) == null) //根据hash值来确认存放的位置。如果当前位置是空直接添加到table中tab[i] = newNode(hash, key, value, null);else {//如果存放的位置已经有值Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p; //确认当前table中存放键值对的Key是否跟要传入的键值对key一致else if (p instanceof TreeNode) //确认是否为红黑树e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {//如果hashCode一样的两个不同Key就会以链表的形式保存for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 判断链表长度是否大于8treeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value; //替换新的value并返回旧的valueafterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();//如果当前HashMap的容量超过threshold则进行扩容afterNodeInsertion(evict);return null;
}

扩容机制核心方法Node<K,V>[] resize()

HashMap扩容可以分为三种情况:

第一种:使用默认构造方法初始化HashMap。从前文可以知道HashMap在一开始初始化的时候会返回一个空的table,并且thershold为0。因此第一次扩容的容量为默认值DEFAULT_INITIAL_CAPACITY也就是16。同时threshold = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12。

第二种:指定初始容量的构造方法初始化HashMap。那么从下面源码可以看到初始容量会等于threshold,接着threshold = 当前的容量(threshold) * DEFAULT_LOAD_FACTOR。

第三种:HashMap不是第一次扩容。如果HashMap已经扩容过的话,那么每次table的容量以及threshold量为原有的两倍。

这边也可以引申到一个问题HashMap是先插入还是先扩容:HashMap是先插入数据再进行扩容的,但是如果是刚刚初始化容器的时候是先扩容再插入数据。

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;//首次初始化后table为Nullint oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;//默认构造器的情况下为0int newCap, newThr = 0;if (oldCap > 0) {//table扩容过//当前table容量大于最大值得时候返回当前tableif (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)//table的容量乘以2,threshold的值也乘以2           newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // initial capacity was placed in threshold//使用带有初始容量的构造器时,table容量为初始化得到的thresholdnewCap = oldThr;else {  //默认构造器下进行扩容  // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;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) {HashMap.Node<K,V> e;if ((e = oldTab[j]) != null) {// help gcoldTab[j] = null;if (e.next == null)// 当前slot没有发生hash冲突,直接对2取模,即移位运算hash &(2^n -1)// 扩容都是按照2的幂次方扩容,因此newCap = 2^nnewTab[e.hash & (newCap - 1)] = e;else if (e instanceof HashMap.TreeNode)// 当前slot节点为红黑树,比较复杂,可以去了解下红黑树算法,这边不进行详解,当树的高度小于等于UNTREEIFY_THRESHOLD则转成链表((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve order// 把当前slot对应的链表分成两个链表,减少扩容的迁移量HashMap.Node<K,V> loHead = null, loTail = null;HashMap.Node<K,V> hiHead = null, hiTail = null;HashMap.Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {// 扩容后不需要移动的链表if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {// 扩容后需要移动的链表if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {// help gcloTail.next = null;newTab[j] = loHead;}if (hiTail != null) {// help gchiTail.next = null;// 扩容长度为当前slot位置+旧的容量newTab[j + oldCap] = hiHead;}}}}}return newTab;}

/*** 测试目的:理解HashMap发生resize扩容的时候对于链表的优化处理:* 初始化一个长度为8的HashMap,因此threshold为6,所以当添加第7个数据的时候会发生扩容;* Map的Key为Integer,因为整数型的hash等于自身;* 由于hashMap是根据hash &(n - 1)来确定key所在的数组下标位置的,因此根据公式 m(m >= 1)* capacity + hash碰撞的数组索引下标index,可以拿到一组发生hash碰撞的数据;* 例如本例子capacity = 8, index = 7,数据为:15,23,31,39,47,55,63;* 有兴趣的读者,可以自己动手过后选择一组不同的数据样本进行测试。* 根据hash &(n - 1), n = 8 二进制1000 扩容后 n = 16 二进制10000, 当8的时候由后3位决定位置,16由后4位。** n - 1 :    0111  &  index  resize-->     1111  &  index* 15    :    1111  =  0111   resize-->     1111  =  1111* 23    :   10111  =  0111   resize-->    10111  =  0111* 31    :   11111  =  0111   resize-->    11111  =  1111* 39    :  100111  =  0111   resize-->   100111  =  0111* 47    :  101111  =  0111   resize-->   101111  =  1111* 55    :  110111  =  0111   resize-->   110111  =  0111* 63    :  111111  =  0111   resize-->   111111  =  1111** 按照传统的方式扩容的话那么需要去遍历链表,然后跟put的时候一样对比key,==,equals,最后再放入新的索引位置;* 但是从上面数据可以发现原先所有的数据都落在了7的位置上,当发生扩容时候只有15,31,47,63需要移动(index发生了变化),其他的不需要移动;* 那么如何区分哪些需要移动,哪些不需要移动呢?* 通过key的hash值直接对old capacity进行按位与&操作如果结果等于0,那么不需要移动反之需要进行移动并且移动的位置等于old capacity + 当前index。** hash & old capacity(8)* n     :    1000  &  index* 15    :    1111  =  1000* 23    :   10111  =  0000* 31    :   11111  =  1000* 39    :  100111  =  0000* 47    :  101111  =  1000* 55    :  110111  =  0000* 63    :  111111  =  1000** 从下面截图可以看到通过源码中的处理方式可以拿到两个链表,需要移动的链表15->31->47->63,不需要移动的链表23->39->55;* 因此扩容的时候只需要把loHead放到原来的下标索引j(本例j=7),hiHead放到oldCap + j(本例为8 + 7 = 15)** @param args*/public static void main(String[] args) {HashMap<Integer, Integer> map = new HashMap<>(8);for (int i = 1; i <= 7; i++) {int sevenSlot = i * 8 + 7;map.put(sevenSlot, sevenSlot);}}

负载因子loadFactor测试

前文提到负载因子loadFactor保持在0.75f是在时间跟空间上达到一个平衡,实际上也就是说0.75f是效率相对比较高的,下面将贴出测试案例。可以看到同样的初始容量但是负载因子不同的map,map2,map4中map4是最快的,而相同的负载因子初始容量map3,map4中map3的速度更快。为此在使用HashMap的时候如果可以预知数据量大小的话最好指定初始容量,可以减少运行过程中扩容的次数,以达到一定的性能提升。

public static void main(String[] args) {HashMap<Integer, Integer> map  = new HashMap<>(16, 100);HashMap<Integer, Integer> map2 = new HashMap<>(16, 0.5f);HashMap<Integer, Integer> map3 = new HashMap<>(100);HashMap<Integer, Integer> map4 = new HashMap<>();DateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss:SS");System.out.println(sdf.format(new Date()));for (int i = 0; i < 5000000; i++) {map.put(i, i);}System.out.println(sdf.format(new Date()));System.out.println(sdf.format(new Date()));for (int i = 0; i < 5000000; i++) {map2.put(i, i);}System.out.println(sdf.format(new Date()));System.out.println(sdf.format(new Date()));for (int i = 0; i < 5000000; i++) {map3.put(i, i);}System.out.println(sdf.format(new Date()));System.out.println(sdf.format(new Date()));for (int i = 0; i < 5000000; i++) {map4.put(i, i);}System.out.println(sdf.format(new Date()));}
20180705152534:83
20180705152546:87
20180705152546:87
20180705152550:643
20180705152550:643
20180705152552:980
20180705152552:980
20180705152556:132

get(Object key)

先前HashMap通过hash code来存放数据,那么get方法一样要通过hash code来获取数据。可以看到如果当前table没有数据的话直接返回null反之通过传进来的hash值找到对应节点(Node)first,如果first的hash值以及Key跟传入的参数匹配就返回对应的value反之判断是否是红黑树,如果是红黑树则从根节点开始进行匹配如果有对应的数据则结果否则返回Null,如果是链表的话就会循环查询链表,如果当前的节点不匹配的话就会从当前节点获取下一个节点来进行循环匹配,如果有对应的数据则返回结果否则返回Null。

final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;//如果当前table没有数据的话返回Nullif ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {//根据当前传入的hash值以及参数key获取一个节点即为first,如果匹配的话返回对应的value值if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;//如果参数与first的值不匹配的话if ((e = first.next) != null) {//判断是否是红黑树,如果是红黑树的话先判断first是否还有父节点,然后从根节点循环查询是否有对应的值if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);do {//如果是链表的话循环拿出数据if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}return null;}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

备战蓝桥杯---搜索(DFS基础1)

何为深搜&#xff1f; 即不撞南墙不罢休。 话不多说&#xff0c;直接看题&#xff1a; 我们可以把这看成深搜的模板题&#xff0c;下面是AC代码&#xff1a; #include<bits/stdc.h> using namespace std; int a[15];//存值并输出 int vis[15]; int n18; void dfs(int …

Redisson看门狗机制

一、背景 网上redis分布式锁的工具方法&#xff0c;大都满足互斥、防止死锁的特性&#xff0c;有些工具方法会满足可重入特性。如果只满足上述3种特性会有哪些隐患呢&#xff1f;redis分布式锁无法自动续期&#xff0c;比如&#xff0c;一个锁设置了1分钟超时释放&#xff0c;…

YOLOv5白皮书-第Y3周:yolov5s.yaml文件解读

YOLOv5白皮书-第Y3周:yolov5s.yaml文件解读 YOLOv5白皮书-第Y3周:yolov5s.yaml文件解读一、前言二、我的环境三、yolov5s.yaml源文件内容四、Parameters五、anchors配置六、backbone七、head八、总结 OLOv5-第Y2周&#xff1a;训练自己的数据集) YOLOv5白皮书-第Y3周:yolov5s.…

从MySQL到TiDB:兼容性全解析

MySQL 在高并发和大数据量场景下&#xff0c;单个实例的扩展性有限。而 TiDB 作为一款分布式NewSQL数据库&#xff0c;设计之初就支持水平扩展&#xff08;Scale-Out&#xff09;&#xff0c;通过增加节点来线性提升处理能力和存储容量&#xff0c;能够很好地应对大规模数据和高…

机器学习——绪论总结

目录 一、引入 二、基本术语 三、假设空间与归纳偏 四、模型选择 一、引入 机器学习&#xff1a;通过计算手段&#xff0c;得出具有能够自我修改、完善能力的模型&#xff0c;利用经验改善系统自身性能。算法使用数据得到模型的过程即称为学习&#xff0c;或训练 流程&…

Framework - ActivityThread 应用启动UI渲染流程

一、概念 ActivityThread拥有 main(String[] agrs) 方法&#xff0c;作为程序的入口&#xff0c;是应用程序的初始化类。&#xff08;ActivityThread不是主线程&#xff0c;它在 main() 方法中实例化&#xff0c;是运行在主线程中。&#xff09;ApplicationThread是 ActivityT…

【Nginx】Ubuntu如何安装使用Nginx反向代理?

文章目录 使用Nginx反向代理2个web接口服务步骤 1&#xff1a;安装 Nginx步骤 2&#xff1a;启动 Nginx 服务步骤 3&#xff1a;配置 Nginx步骤 4&#xff1a;启用配置步骤 5&#xff1a;检查配置步骤 6&#xff1a;重启 Nginx步骤 7&#xff1a;访问网站 proxy_set_header 含义…

海外IP代理:解锁网络边界的实战利器

文章目录 引言&#xff1a;正文&#xff1a;一、Roxlabs全球IP代理服务概览特点&#xff1a;覆盖范围&#xff1a;住宅IP真实性&#xff1a;性价比&#xff1a;在网络数据采集中的重要性&#xff1a; 二、实战应用案例一&#xff1a;跨境电商竞品分析步骤介绍&#xff1a;代码示…

LeetCode--189

189. 轮转数组 提示 给定一个整数数组 nums&#xff0c;将数组中的元素向右轮转 k 个位置&#xff0c;其中 k 是非负数。 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转…

Ingress

文章目录 环境准备什么是 Ingress认识 Ingress 资源Ingress 控制器(controller)Ingress 规则pathType 路径类型多重匹配Ingress 类TLS生成证书创建密钥 环境准备 下面的 yaml 文件内容&#xff0c;是使用 sts 创建两个 web 服务&#xff0c;并配置对应的 servcie。web 服务的首…

微信小程序(三十三)promise异步写法

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.promise异步与普通异步的写法区别 2.promise异步的优势 源码&#xff1a; index.wxml <view class"preview" bind:tap"onChoose"><image src"{{avatar}}" mode"…

网络时间协议NTP工作模式

单播服务器/客户端模式 单播服务器/客户端模式运行在同步子网中层数较高层上。这种模式下,需要预先知道服务器的IP地址。 客户端:运行在客户端模式的主机(简称客户端)定期向服务器端发送报文,报文中的Mode字段设置为3(客户端模式)。当客户端接收到应答报文时,客户端会…

Chapter One - The History of Computers

Chapter One - The History of Computers 第一章 - 计算机的历史 I. Reading Material I. 阅读材料 My friends, let’s embark on an enlightening journey through the captivating history of computers, unraveling the intricate threads that have woven the technolog…

DockerCompose+SpringBoot+Nginx+Mysql实践

DockerComposeSpringBootNginxMysql实践 1、Spring Boot案例 首先我们先准备一个 Spring Boot 使用 Mysql 的小场景&#xff0c;我们做这样一个示例&#xff0c;使用 Spring Boot 做一个 Web 应 用&#xff0c;提供一个按照 IP 地址统计访问次数的方法&#xff0c;每次请求时…

linux交叉编译方法——虚拟机编译,在树莓派平台上运行

一、 交叉编译是什么 交叉编译 是在一个平台上生成另一个平台上的可执行代码。 我们再windows上面编写C51代码&#xff0c;并编译成可执行代码&#xff0c;如xx.hex, 是在c51上面运行&#xff0c;不是在windows上面运行 我们在ubuntu上面编写树…

Kubernetes k8s

Kubernetes k8s 一个开源的容器编排引擎&#xff0c;用来对容器化应用进行自动化部署、 扩缩和管理。 从架构设计层面&#xff0c;k8s能很好的解决可用性&#xff0c;伸缩性&#xff1b;从部署运维层面&#xff0c;服务部署&#xff0c;服务监控&#xff0c;应用扩容和故障处…

springboot并mybatis入门启动

pom.xml,需要留意jdk的版本&#xff08;11&#xff09;和springboot版本要匹配&#xff08;2.7.4&#xff09;&#xff0c;然后还要注意mybatis启动l类的版本&#xff08;2.2.2&#xff09; <?xml version"1.0" encoding"UTF-8"?> <project xm…

MAX31865读取PT100/PT1000电阻值

1、芯片介绍 MAX31865是简单易用的热敏电阻至数字输出转换器,优化用于铂电阻温度检测器(RTD)。外部电阻设置RTD灵敏度,高精度Δ- Σ ADC将RTD电阻与基准电阻之比转换为数字输出。MAX31865输入具有高达45V的过压保护,提供可配置的RTD及电缆开路、短路条件检测。 2、芯片特点…

解决Docker打包Eureka注册中心,其他服务无法注册问题

​前言 本文主要是介绍利用docker打包Eureka注册中心&#xff0c;并且发布镜像到服务器&#xff0c;遇到的一个比较坑的问题。主要是服务镜像部署完毕之后&#xff0c;docker容器都能启动&#xff0c;并且也能访问&#xff0c;但是其他服务就是无法注册到注册中心。排除问题&a…

查看阿里云maven仓中某个库有哪些版本

起因 最近项目上有做视频业务&#xff0c;方案是使用阿里云的短视频服务&#xff0c;其中也有使用到阿里云的上传SDK&#xff0c;过程中有遇一个上传SDK的内部崩溃&#xff0c;崩溃栈如下&#xff1a; Back traces starts. java.lang.NullPointerException: Attempt to invok…