HashMap的基础概念及其扩容机制

一、HashMap 简介

​ HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一。

二、HashMap结构

在JDK1.7中,HashMap数据结构为数组+链表;
HashMap在JDK 1.8,底层是由“数组+链表+红黑树”组成,如下图,而在 JDK 1.8 之前是由“数组+链表”组成。
![![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/545f6e0946a844d89ee6a1a95198adfb.png]

为什么要改成“数组+链表+红黑树”?

主要是为了提升在 hash 冲突严重时(链表过长)的查找性能,使用链表的查找性能是 O(n),而使用红黑树O(logn)。

那在什么时候用链表?什么时候用红黑树?

对于插入,默认情况下是使用链表节点。当同一个索引位置的节点在新增后达到9个(阈值8):如果此时数组长度大于等于 64,则会触发链表节点转红黑树节点(treeifyBin);而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。

对于移除,当同一个索引位置的节点在移除后达到 6 个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点(untreeify)。

三、HashMap的基础变量配置解释

  • 默认初始容量为16

  • 最大长度为2的30次幂

  • 默认加载因子为0.75

  • 当链表节点小于等于6,自动退化成链表

  • 当链表节点大于等于8,长度大于64时进行变化成红黑树

  • 扩容阈值,当你的hashmap中的元素个数超过这个阈值,便会发生扩容

  • threshold(存储大小阈值) = capacity(容量) * loadFactor(默认加载因子)

 //HashMap的默认初始长度16
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
  //HashMap的最大长度2的30次幂
  static final int MAXIMUM_CAPACITY = 1 << 30;

  //HashMap的默认加载因子0.75
  static final float DEFAULT_LOAD_FACTOR = 0.75f;

  //HashMap链表升级成红黑树的临界值
  static final int TREEIFY_THRESHOLD = 8;

  //HashMap红黑树退化成链表的临界值
  static final int UNTREEIFY_THRESHOLD = 6;

  //HashMap链表升级成红黑树第二个条件:HashMap数组(桶)的长度大于等于64
  static final int MIN_TREEIFY_CAPACITY = 64;

四、HashMap的扩容机制

1、添加数据机制,调用put方法过程

①.判断键值对数组table[i]是否为空或为null,是的话执行resize()进行扩容;

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

在这里插入图片描述

2、HashMap的扩容操作是怎么实现的?

对于插入,默认情况下是使用链表节点。当同一个索引位置的节点在新增后达到9个(阈值8):如果此时数组长度大于等于 64,则会触发链表节点转红黑树节点(treeifyBin);而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。对于移除,当同一个索引位置的节点在移除后达到 6 个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点(untreeify)。

①.在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;

②.每次扩展的时候,新容量为旧容量的2倍;

③.扩展后元素的位置要么在原位置,要么移动到原位置 + 旧容量的位置。

在putVal()中使用到了2次resize()方法,resize()方法在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方;
在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+旧容量的位置。

五、HashMap扩容操作是尾插还是头插?

1、JDK1.7扩容:

条件:

发生扩容的条件必须同时满足两点

当前存储的数量大于等于阈值和发生hash碰撞
特点:先扩容,再添加(扩容使用的头插法)
缺点:头插法会使链表发生反转,多线程环境下可能会死循环
扩容之后对table的调整:table容量变为2倍,所有的元素下标需要重新计算

2、JDK1.8扩容:

条件:当前存储的数量大于等于阈值
当某个链表长度>=8,但是数组存储的结点数size() < 64时
特点:先插后判断是否需要扩容(扩容时是尾插法)
缺点:多线程下,1.8会有数据覆盖
扩容之后对table的调整:table容量变为2倍,但是不需要像之前一样计算下标,只需要将hash值和旧数组长度相与即可确定位置

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值