HashMap详细解析

HashMap是Java中基于数组和链表的数据结构,采用负载因子0.75f和默认初始容量16。在Java 8中,当链表长度达到8时会转换为红黑树。插入操作在Java 8之前使用头插法,之后改为尾插法以避免扩容后的链表环状问题。扩容发生在容量达到当前容量的0.75倍时,新数组长度为原数组的2倍,并需要重新哈希所有元素。尽管Java 8后的HashMap在扩容后能保持数据顺序,但它不是线程安全的,不适合多线程直接使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、 基础认知

HashMap是由数组和链表组合构成的数据结构。

默认负载因子 loadFactor 负载因子,默认值0.75f
默认初始化长度 16
链表存储模型:

java8中引入

  • 存储元素小于等于6时为链表
  • 元素为7时不做改变
  • 元素为8时变为红黑树

因为他本身所有的位置都为null,在put插入的时候会根据key的hash去计算一个index值。

数据本身是通过key值得hash值来进行存储,虽然hashmap提供的是散列存储 意思是使数据均匀的分布在不同的hash位置上 但实际场景中还是有很多key值是属于同一个hash值下的 意思就是存储在数组的同一个位置上 而在这个位置上存储的就是一个链表 在查询时 会先通过key值得hash值 找到hash值所在的链表 然后在链表上递归查询出对应的值

HashMap中存储的每个元素 都是存储在一个Node<K, V>对象中的 在这个对象中会存储 key的hash值 key值 value值及下一个元素 所以hashmap中的链表是一个单向链表

2、 链表插入方法
java8之前是头插法,就是说新来的值会取代原有的值,原有的值就顺推到链表中去,就像上面的例子一样,因为写这个代码的作者认为后来的值被查找的可能性更大一点,提升查找的效率。
但是,在java8之后,都是所用尾部插入了。

但是为什么会有存头存尾的改变呢?

首先要看一下HashMap如何扩容
扩容发生的前提由两个参数影响 一个是capacity容量,LoadFactor负载因子,默认值0.75f
意思就是 如果一个容量为100的map,当你存入第76个的时候就需要扩容了 也就是resize

扩容主要分为两步

  1. 扩容:创建一个新的Entry空数组,长度是原数组的2倍
  2. ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组
    需要rehash的原因
    因为hash的公式是 HashCode(Key) & (Length - 1)
    所以长度扩大之后,hash的规则也就随之改变

其次需要知道的是jdk1.7时的头插方法有什么弊端
举例:
我们要在容量为2的容器里面用不同线程插入A,B,C,假如我们在resize之前打个短点,那意味着数据都插入了但是还没resize那扩容前可能是这样的。

我们可以看到链表的指向A->B->C

而扩容resize之后
因为resize的赋值方式,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。
如果此时AB的hash值还是相同 那么就会出现一种情况 因为赋值方式是头插 所以这时先存的是B元素 然后放A 这时B指向了A A指向的是B 一旦开始查找就显示了死循环

 Java7在多线程操作HashMap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。

而使用头插会改变链表的顺序,但是如果使用尾插在扩容是就会保持链表元素原本的顺序,就不会出现链表成环的问题了

java8之后链表有红黑树的部分,大家可以看到代码已经多了很多if else的逻辑判断了,红黑树的引入巧妙的将原本O(n)的时间复杂度降低到了O(logn)。(红黑树知识待学习!!!)

虽然多线程中 java1.8扩容后依然能保持数据顺序的一致 但是并不能保证在多线程中直接使用hashmap 因为put/get方法并没有加同步锁 你就无法保证 上一秒put的和下一秒get的是可能还是原值 所以线程安全并不保证

3、 初始化大小为什么是16呢?
直接粘代码
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

首先为啥写成1<<4呢 ?
因为java实际上还是基于2进制算法来编译和运行的 所以这样写实际能帮助 jvm编译时更加快速的进行编译
上面说了 既然是2进制的算法 那么在扩容时当时也是2的幂数 更方便内存的读写和运算了
但是为啥是16嘞? 感觉还是编码大人的习惯吧 或者觉得这个数在日常的编码过程中 更平均一些 能满足大多数场景的基本map使用

学习自
https://juejin.im/post/5dee6f54f265da33ba5a79c8 敖丙大神

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值