HashMap
参考链接:
- https://tech.meituan.com/2016/06/24/java-hashmap.html
- https://juejin.im/post/6844903588179755021#heading-14
| HashMap就是使用哈希表来存储的。哈希表为解决冲突,可以采用开放地址法和链地址法等来解决问题,Java中HashMap采用了链地址法。
| 链地址法,简单来说,就是数组加链表的结合。
当链表的长度大于8的时候,转换为红黑树。
横的是数组,竖的是链表
[4]
|
[3]
|
[2]
|
[1]
|
[1][2][3][4][5]
计算bucket下标
Hash算法本质:取Key的hashcode值、高位运算、取模运算
方法一:
static final int hash(Object key) { //jdk1.8 & jdk1.7
int h;
// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16) 为第二步 高位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
方法二:
static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
return h & (length-1); //第三步 取模运算
}
详细可以参考:https://juejin.im/post/6844903588179755021#heading-11
扰动
扰动:为了减少冲突,因为直接计算hashcode的话发生冲突的概率很高
hash方法中进行扰动的解释:
https://www.zhihu.com/question/51784530
Put方法
添加元素过程:
如果 Node[] table 表为 null ,则表示是第一次添加元素,讲构造函数也提到了,及时构造函数指定了期望初始容量,在第一次添加元素的时候也为空。这时候需要进行首次扩容过程。
计算对应的键值对在 table 表中的索引位置,通过i = (n - 1) & hash 获得。
判断索引位置是否有元素如果没有元素则直接插入到数组中。如果有元素且key 相同,则覆盖 value 值,这里判断是用的 equals 这就表示要正确的存储元素,就必须按照业务要求覆写 key 的 equals 方法,上篇文章我们也提及到了该方法重要性。
如果索引位置的 key 不相同,则需要遍历单链表,如果遍历过如果有与 key 相同的节点,则保存索引,替换 Value;如果没有相同节点,则在但单链表尾部插入新节点。这里操作与1.7不同,1.7新来的节点总是在数组索引位置,而之前的元素作为下个节点拼接到新节点尾部。
如果插入节点后链表的长度大于树化阈值,则需要将单链表转为红黑树。
成功插入节点后,判断键值对个数是否大于扩容阈值,如果大于了则需要再次扩容。至此整个插入元素过程结束。
扩容resize方法
扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。
疑问:最大存储容量为什么是 2的30次方
面试题:
HashMap线程不安全在哪?
- 数据覆盖问题
- 1.7 扩容时容易导致死循环
https://juejin.im/post/6844904013909983245#heading-28
如何是规避HashMap的线程不安全?
- 将Map转为包装类
Map<String, Integer> testMap = new HashMap<>();
...
// 转为线程安全的map
Map<String, Integer> map = Collections.synchronizedMap(testMap);
- 使用ConcurrentHashMap
Map<String, Integer> susuMap = new ConcurrentHashMap<>();
并发环境下应该使用ConcurrentHashMap