Hash:散列
Map:映射
顾名思义,是以 key-value 的形式存储数据
public class HashMap<K,V> {
transient Node<K,V>[] table;
// 初始容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
}
通过源码可知,他是一个类型为 key-value 形式的数组,key 的 hash(不仅仅是取hash这么简单,后续会讲)值决定了数据应该存放在数组的哪个下标里面,存放在数组里面的数据格式为链表,在 jdk1.8 中极其之后,数据格式引入了树型结构,会在一些特殊情况下发生链表与红黑树的相互转换
HashMap 的特点
无序
存储的顺序和取出的顺序不一定一样,这也是他的缺点
key可以为null
当 key 为 null 时,默认 hash 值为0,所以说可以存储 key 为 null 的数据
key不可以重复
相同的 key 数据会被覆盖,这是符合正常的认知的
hash冲突
为什么会产生 hash 冲突呢,前面讲过,key 的 hash 值决定了数据应该存放在数组的哪个下标里面
而为了防止数组下标越界,那么这个 hash 的计算方式需要保证 hash 的取值范围在数组的下标范围内
这样就会产生不同的 key 计算出相同的 hash,这就是 hash 冲突,也叫 hash 碰撞
如果直接进行 value 的覆盖操作,那么就会出问题,所以说在 key 的 hash 值相同,key 不同时,直接挂在了当前下标中的链表的下一个位置
如下图所示,链表中存储的 Node 节点包含四个数据:key、value、hash、下一个 Node
那么这个具体的 hash 值是怎么算的呢,有没有一个巧妙的算法能让计算出来的 hash 值刚好在数组中间且不会出现数组越界呢?
有一个好的方式就是先算出 key 的真实 hash 值,然后根据数组的长度取模
直接用长度的取模出来的值在1 - size 之间,0永远不会取到且取到 size 时又越界了,所以说直接数组的长度 -1 取模就好了,这是有些人不明白为什么减一的原因
但是在 HashMap 中,并没有采取以上说的取模的操作,而是用了更取巧的操作,与运算
不管真正的 hash 值是多少,忽略前面的具体值,只取最后四位二进制值进行与运算,还需要确保在 0000 - 1111 之间,这也需要数组的长度必须是2的整次方数,比如 2、4、8、16、32等,这也解释了为什么 HashMap 中的数组长度默认是 16,以及每次扩容都是在之前的基础上乘2的原因
那么如果仅仅是对 key 的值进行 hash 算法然后取后四位,那么发生碰撞的概率依然很大,会出现很多值存在同一个链表上,其他链表没有值,所以说不仅仅是 hash 计算,而且进行了二次 hash,也叫做扰动函数,目的是让更多位置参与到 hash 计算中来,破坏掉直接 hash 的扎堆概率
树 、链 转化
在 jdk1.8 中,数组中的每个下标存储的链表,在链表长度大于8且数组容量 >= 64 时,链表中的数据会进行树化(红黑树,是一种自平衡的二叉树),主要是为了提升查找速度,链表一旦长了查询就会很慢;
当链表长度小于8时,又会从红黑树转化为链表,当数据量少的时候,链表与红黑树的查询速度不相上下,且在新增元素的时候,链表不需要计算节点的位置(红黑树是需要计算的)
相互转化可以形成很好的互补
扩容
扩容因子,也可以说是满载率,默认是 0.75,
具体来说当元素超过整体空间 75% 的时候,并且新元素要添加的数组位置不为空的时候就要进行数组长度扩容了
也就是说当元素超过整体空间 75% 的时候并不一定会触发扩容,还需要看新元素要添加的位置是否有值存在,如果没有是不会进行扩容的
HashMap 的创建与常用方法
put方法
在 put 中才开始创建数组
在 jdk1.7 中,元素的添加在链表上采用的是头插法(1.7的设计者认为,新插入的元素在后续的过程中有更大的概率被使用到)
总结一下 HashMap 的存值过程:
- 先通过二次 hash 得到一个扰动后的 hash 值
- 然后进行与运算确定需要存放到哪个数组下标的链表当中
- 然后确定是否需要扩容
- 不需要扩容则直接头插法插入
- 需要扩容:
- 则创建一个是原来长度2倍的数组
- 将之前的数组中的所有元素的 key 根据现在的新数组长度进行新的与运算,只有50%的概率结果会跟之前的相同(key的二进制当中,后四位不会发生改变,只有倒数第五位有可能变化,取值可能性是0、1,所以说是50%)
- 然后更新 hash,并将 Node 存储到新数组的新下标对应的链表当中
- 然后再进行新的 hash (数组长度改变了)
- 最后进行数据的插入操作
get方法
先进行对 key 的二次 hash,然后与运算找到数组的索引,通过索引找到当前索引的链表,在链表中遍历,通过 hsah 值相等且 key 的值也相等,找到 Node 节点的 value
HashMap 缺点
- 无序:存储的顺序和取出的顺序不一定一样
- 线程不安全的