HashMap数据结构及扩容详解、指定默认容量实例化后不一致问题详解

1、HashMap底层存储数据结构

HashMap是Map的实现类,是一个集合类,存储的是<K,V>键值对数据。
HashMap底层存储的数据结构如下:
在JDK1.7及前

  • 数组
  • 链表

在JDK1.8后

  • 数组
  • 链表 -当链表的长度临近于8时,转为红黑树
  • 红黑树

2、HashMap扩容

HashMap的成员变量(部分):
Size:表示map中有多少个元素,默认为0
DEFAULT_INITIAL_CAPACITY:表示集合的初始容量,默认16
MAXIMUM_CAPACITY:表示集合的最大容量,默认1073741824
DEFAULT_LOAD_FACTOR:负载系数,默认值0.75
TREEIFY_THRESHOLD:链表转换红黑树的阈值,默认为8,表示当链表长度临近此值时,链表将转为红黑树
threshold:下一个要调整大小的大小值,默认0,在不指定初始容量时,其初始化值为(DEFAULT_INITIAL_CAPACITY * loadFactor),即为12,之后每次扩容都是 << 1,即2倍
loadFactor:哈希表的负载因子

HashMap的put其实调用的是putVal()方法,底层保存的节点上都是一个node对象,node是HashMap中的一个内部类,node变量如下:
在这里插入图片描述
所以HashMap底层数据结构与LinkedLlist有相似之处,在putVal()方法中,会判断集合中是否有值,如果没有则会调用 resize() 方法进行第一次扩容操作。

注:
在向hashMap中添加数据时,会存在一种问题,hash碰撞,即hash算法返回的存储位置是一样的,这时存储的数据结构就由原来的数组变为链表;
即经过hash算法后,返回一个下标值,数据在存储往该下标位置存储时,会发现该位置不为空,这样的现象就是hash碰撞,这时存储时会将原来存储在该位置的对象下移,将本次添加的对象添加到该位置,同时把下移的对象的引用赋值给本次添加对象的next属性上。
在这里插入图片描述
再看 resize() 方法,扩容的情况有三种,如下

  1. 初始化未给定初始容量扩容
    初始进来,table因为集合中没有元素,所以为null,threshold默认为0,所以根据代码逻辑初始进来会进入到else中,给定新的初始容量(newCap) = 默认的容量16,新的初始大小值(newThr) = 负载系数 * 默认容量值 = 12;
  2. 初始化给定初始容量扩容
    但是如果在new HashMap(7)给定初始容量时,此时table仍然为null,所以oldCap = 0,而threshold = 8(具体这里为何为8,在本文最后说明),如下源码中因为这里oldCap=0,所以 if 判断为false,进入else if 中,设置newCap = oldThr即为8;然后第一个if判断完成;到第二个 if 条件判断newThr==0,因为newThr值为0进入if,ft=newCaploadFactor=80.75;判断没有超过最大容量值,所以newThr = 8 * 0.75 = 6,if 后面赋值给threshold=newThr。所以此时的集合容量为8,调整大小值为6,也就是说集合只要容量超过6就会进行扩容。
  3. 集合满后扩容
    集合满后(判断集合元素是否满代码逻辑解释可在后面查看)扩容,table中是有值的,所以oldCap > 0,进入第一个if,然后判断是否大于最大限制容量,如果是返回最大限制容量值,否则,赋值newCap = oldCap << 1 小于最大限制容量 且oldCap >= 初始化容量,都成立则newThr = oldThr <<1;
    上述三种扩容,最终在扩容方法中都会进入到如下的if(oldTab != null){}中进行元素复制扩容,扩容后把原来的集合放到新集合中,这里介绍一篇文章,里面有具体的扩容详解
    在这里插入图片描述
    扩容完成后,在putVal()方法的最后会有一个判断,判断size是否大于threshold,如果大于则进行重新扩容,否则put完成。这里判断主要是为了如果集合元素存入临近限定的容量大小时进行扩容操作。
    注意,这里不是和集合的容量比较,而是和集合的调整大小值比较,如上,容量为8,而调整大小值为6,这里会和6进行比较,所以当元素个数超出此值时就会进行扩容,之后的扩容每次扩容为原来的2倍。
    在这里插入图片描述

3、HashMap指定初始值源码计算实现介绍

这里说明下上面提到的给定初始值于实例化后的初始值不一致问题
其实简单总结就是如果指定的初始容量不是2的幂次方,则会重新计算指定初始容量为向上的最近的2的幂次方的数值为集合容量。比如指定为6,则计算后为8,指定为8则默认为8,指定为12,默认计算为16
以下是源码中的计算方法:
在这里插入图片描述
在HashMap中,默认 初始容量为16,也可以自己指定初始容量值,但在HashMap实例化时,实例化的初始容量值可能不是你指定的初始容量值,如你指定为5,则HashMap实例化后的初始容量值为8,如果你指定的是8,则实例化后的容量为8。就是它会实例化为2的n次幂后的一个值作为初始值,且这个值是你给定值向上去取的一个2的n次幂的值,这个值是在HashMap中有一个算法进行计算得到的,如下:
在HashMap指定容量实例化时会调用此方法,参数为你指定的初始容量值,
这里计算的方法是,首先会将给定的值减1然后赋值给n,然后用n去做按位或运算 “|=” 和移位运算 “>>>” ,
首先介绍按位或运算,“|=”其实和“+=”相似,如a |= b,表示a与b按位运算后赋值给a,像a += b表示a = a+b;按位或运算可以把a、b转换为二进制如下图,假设a = 4,b = 2;
在这里插入图片描述
再说移位运算 “>>>” 表示无符号右移,如4 >>> 1 = 2,计算方法是把4转换为二进制为0100,像右移1移位,高位补0,移位后为0010 = 2;
所以这里如果传入初始值为5,则n = 4,n |= n >>> 1;即表示 4 |= 4 >>> 1,n = 4 | 2;得n=6,然后继续运算,n |= n >>>2;同上运算得n = 7;然后继续,n |= n >>>4;注意,这里7转二进制为0111,右移位4位后为0000,则n = 7 | 0 = 7;后续同是,所以最后得n等于7;
然后在返回值中做判断,如果n < 0则返回1,否则判断如果n>最大限度值,则返回最大限定值,否则返回n + 1,这里计算得8,所以如果输入初始容量为5,则HashMap实例化时计算得到的默认初始值为8。

所以由上面我们可以知道,在我们指定初始容量的时候,其实hashmap的默认扩容的大小值其实是小于我们指定的容量的,比如我们指定的是8,但其实size大于6后就会扩容,这样就不符合我们的预期,它依然会进行一次扩容操作,那么怎么办呢,源码中其实给了我们一个计算公式,expectedSize / 0.75F + 1.0F就是我们预期的容量除以0.75再加1,如指定为8,则计算下来是8/0.75+1。计算得到11,这样,它默认的容量就是16,如此就符合我们的预期,保存我们指定的数据不会进行扩容操作。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值