HashSet的底层就是调用的HashMap,所以在之前的笔记实际上已经部分讲解过HashMap。
1.HashMap特点小结
- key不能重复,但是值可以重复,允许使用null键null值。键和值位置都可以是 null,但是键位置只能存在一个 null。
- 如果添加相同的key,则会覆盖原来的key-value,等同于修改(key不换 value换)
- jdk1.8 前数据结构是链表+数组,jdk1.8 之后是链表+数组+红黑树。
- 与HashSet一样,不保证映射的顺序(存储无序的),因为底层是以hash表的方式来存储的
- 阈值(边界值)> 8 并且数组长度大于 64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询。
- HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有Synchronized
2.JDK1.7 VS JDK1.8
不同
|
JDK 1.7
|
JDK 1.8
|
存储结构
|
数组
+
链表
|
数组
+
链表
+
红黑树
|
初始化方
式
|
单独函数:
inflateTable()
|
直接集成到了扩容函数
resize()
中
|
hash
值
计算方式
|
扰动处理
= 9
次扰动
= 4
次位运
算
+ 5
次异或运算
|
扰动处理
= 2
次扰动
= 1
次位运算
+ 1
次异
或运算
|
存放数据
的规则
|
无冲突时,存放数组;冲突
时,存放链表
|
无冲突时,存放数组;
冲突
&
链表长度
<8
:存放单链表;
冲突
&
链表长度
> 8
:树化并存放红黑树
|
插入数据
方式
|
头插法(先讲原位置的数据移
到后
1
位,再插入数据到该位
置)
|
尾插法(直接插入到链表尾部
/
红黑树)
|
扩容后存
储位置的
计算方式
|
全部按照原来方法进行计算(即
hashCode ->>
扰动函数
->> (h&length-1)
)
|
按照扩容后的规律计算(即扩容后的位置=
原位置
or
原位置
+
旧容量)
|
3.底层机制
(1)结构
注:每个节点都是HashMap$Node类型
(2)扩容机制 与HashSet完全一致
- 第一次添加时,table数组扩容到16,临界值(threshold)=16×0.75(负载因loadFactor)=12
- 如果table的size达到12,就会二倍扩容扩容到16×2=32, 新的临界值=32×0.75=24,以此类推。
扩展后 Node 对象的位置要么在原位置,要么移动到原偏移量两倍的位置。注:每次加入一个元素 table的size就会++,不一定非要占用数组的个数达到临界值才扩容。即挂载在链表上的元素+数组里的元素到达临界值就扩容。
(3) put方法的具体流程总结
4.其他注意
(1)HashMap如何解决冲突
解决冲突的方法:
- 链表:链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;
- 数组:开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。
遇到的问题即HashMap为什么不直接使用hashCode()处理后的哈希值直接作 为table的下标?
答:
- hashCode() 方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30。 即HashCode的范围 远大于 table下标范围
- 从而导致通过 hashCode() 计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置; 这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表。
上面提到的问题,主要是因为如果使用
hashCode
取余,那么相当于
参与运算的只有
hashCode
的
低位
,高位是没有起到任何作用的,所以我们的思路就是让
hashCode
取值出的高位也参与运算,
进一步降低
hash
碰撞的概率,使得数据分布更平均,我们把这样的操作称为
扰动
,
在
JDK 1.8
中的hash()
函数如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// 与自己 右移16位进行异或运算(高低位异或)
}
这比在
JDK 1.7
中,更为简洁,
相比在
1.7
中的
4
次位运算,
5
次异或运算(9
次扰动),在
1.8
中,只进行了
1
次位运算和
1
次异或运算(2
次扰动)
;
(2)能否使用任何类作为Map的Key?
可以使用任何类作为
Map
的
key
,然而在使用之前,需要考虑以下几点:
- 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。
-
类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。
-
如果一个类没有使用 equals() ,不应该在 hashCode() 中使用它。
即若使用Object作为HashMap的Key,应该怎么办呢?
答:重写 hashCode() 和 equals() 方法
重写 hashCode() 是因为需要计算存储数据的存储位置 ,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的 Hash 碰撞; 重写 equals() 方法 ,需要遵守自反性、对称性、传递性、一致性以及对于任何非 null 的引用值 x , x.equals(null) 必须返回 false 的这几个特性, 目的是为了保证 key 在哈希表中的唯一性 ;
(3)为什么HashMap中String、Integer这样的包装类适合作为K?
答: String 、 Integer 等包装类的特性能够保证 Hash 值的不可更改性和计算准确性,能够有效的减少 Hash 碰撞的几率
都是 fifinal 类型,即不可变性,保证 key 的不可更改性,不会存在获取 hash 值不同的情况 内部已重写了 equals() 、 hashCode() 等方法,遵守了 HashMap 内部的规范(不清楚可以去上面看看 putValue 的过程),不容易出现 Hash 值计算错误的情况;