简介
点击查看【bilibili】
感谢!!!视频2piece共2个多小时。
快速入门
什么是hashmap
HashMap是hash表对map接口的实现,是数组和链表的结合,HashMap同时兼备快速插入和读取的特性。
hashmap提供所有可选的映射操作, 并允许使用Null值和null键。
hashmap并非线程安全,当多个线程写入hashmap时,可能会导致数据不一致。
源码重要全局变量
loadFactor 表示负载因子,默认0.75,当负载因子高于0.75会扩容。
threshold表示所能容纳的键值对的临界值,计算公式为:数组长度*负载因子
modCount字段用来记录hashmap内部结构发生变化的次数。
常量INITIAL_CAPACITY 初始容量为16
常量TREEIFY_THRESHOLD = 8 树化临界值 大于8转红黑树
常量UNTREEIFY_THRESHOLD =6 仅在resize时使用,桶内元素<=6个,存链表。
常量MIN_TREEIFY_THRESHOLD = 64 最小树化容量
结构
hashmap使用数组+链表的数据结构
hashmap数组部分称为 hash桶 。当链表长度大于等于8时,链表数据将以红黑树存储,当长度降到6时会转到单向链表
链表遍历的时间复杂度为O(n)
红黑树的时间复杂度为O(log n)
如何设计hashmap?
二进制运算复习
hash
hash值:通过一定的散列算法,把一个固定的输入,转成一个固定的输出,在hashmap中,hash就一个Int值
hashcode计算:
取string的char字符的ascii码,然后再做一定的运算?
hash函数:把hash值打散到 数组的桶 索引位
hash碰撞:不同hash值,通过hash函数 被分配到相同的hash桶。
有哪些hash冲突的解决办法?
1. 再hash
2. 开放寻址
3. 开辟公共溢出区
4. 链地址法(hashmap)
-
如何让设计的hashmap存取效率更高?
已经说了是hashmap,就不要再讲数组+链表,继承数组的快速读和链表的快速插
- 尽可能少的产生hash冲突
- 确实发生hash冲突,使用最佳的数据结构来保证hash桶内的数据最优存取
Hash函数
hash值 -> 数组索引值
// 伪代码: 拿hash值,计算数组索引值
static int indexForHash(int hash, int lenght){
return hash & (lenght - 1);
}
为什么hashmap的lenght一定要是2的幂次方? 为什么扩容要以2的倍数?
- 位运算比取模运算快27倍。用lenght-1 和 hash值的二进制 与运算 得到的值总是 [0,lenght-1],有取模相同的效果但性能更优. (只有和2的n次方的&运算能够有取模相同的效果,不要再问了)
因此,这个的hash函数的离散率依赖hashCode值。hashcode值越分散,hash冲突越少。
- 1.8以后 resize后不需要rehash 老数据桶下标=旧下标 或者(旧下标+扩容长度)
resize 扩容
什么时候扩容?
JDK 1.7
扩容后再加元素
add时,负载因子高于0.75 且 当前桶下标产生hash碰撞
JDK 1.8
怎么扩容?
JDK1.7
常规操作: 扩容 , rehash然后数据迁移
存在问题:头插法造成单线程迁移产生死环
JDK1.8
尾插法 add
桶内为单链表,对单链表迁移
桶内为红黑树(同时也是双向链表),将双向链表进行数据迁移。
为什么桶内元素个数>=8时转红黑树?
根据泊松分布,桶内元素=8个的概率=亿分之6,小于亿分之十,树化的概率很低,但树化后对get的效率提升很高。
扩容后有什么问题(多线程环境)?
jdk1.7
jdk1.8
多线程下 会有数据丢失的问题
什么样的hash函数是最好的?
负载因子低,使数据离散地分布在每个hash桶里面。或者说hash碰撞最少。
源码级解读
put原理
JDK 1.7
public V put(K k, V v){
if(k==null) return putForNullKey(v);
// 内部通过一个扰乱算法 得到一个hash值 用于计算数组索引
int hash = hash(key);
// 计算数组桶下标
int i = indexFor(hash,table.length);
// 判断是否已存在 重复键
for( e at 桶内元素集 ){
if (e.hash==hash && (e.key == k || key.equals(k))){
// 更新k对应的value值
V old = e.value;
e.value = value;
e.recordAccess(this);
return old;
}
}
modCount++;
// key不存在 添加新元素
addEntry(hash,key,value,i);
return null;
}
void addEntry(int hahs,K k, V v, int bucketIndex){
if(size>= threshold && null!=table[bucketIndex]){
// 负载高于0.75 且 当前桶位非空
// 扩容为两倍
resize(2*table.length);
// rehash
hash= null==key?0:hash(key);
buckeyIndex = indexFor(hash,table.length);
}
// 添加kv到bucketIndex
createEntry(hash,k,v,bucketIndex);
}
jdk1.8
public V put(int hash, K k, V v, boolean evict){
int i = (table.length-1)&hash
// table初始化
if (table==null || table.length==0){
n = (table = resize()).length;
}
// 当前桶位 无元素 直接放入
if(table[i]==null) {
table[i]=newNode(hash,k,v,null);
}else{
// 存在hash碰撞
Node record = table[i];
if(record.key==k){
根节点的Key刚好=k, 更新v
}else if(当前桶内集合为红黑树){
record = putTreeVal(..);
}else{
桶内集合为单链表
for(ele at 链表all){
record = ele;
if(record.next==null){
插入链表尾部
if (链表size>8) treeify(...); 添加到树();
break;
}
if(record.key=k) break; // key重复
}
}
if(record!=null){
// 如果插入已存在的key
更新record值
return record;
}
}
// key不存在于新增 且 新增后 超出临界值 直接扩容
if(++size>threshold)
resize(...)
afterNodeInsertion(evict)
return null
}
扩容
jdk1.7
void resize(int newCapacity){
扩容;
newTable = new Entry[newCapacity];
transfer(newTable,rehash);
threshold = 0.75*newCapacity;
table = newTable;
}
void transfer(newtab,rehash){
int newcapacity = newtab.length;
for (e at table){
while(null != e){
// 重新计算hash值,数据从老表迁移到新表
next = e.next;
if(rehash){
e.hash==null?0:hash(e.key);
}
int i = indexFor(e.hash,newCapacity);
newTable[i] = e;
e=next;
}
}
}
jdk1.8
void resize(){
计算new阈值,newCapacity, 初始化table;
table = newTable
// 执行迁移
for (e at oldTable){
if(e.next==null) {
直接put到newTable;
}else if (e instance of TreeNode){
// spilt树到新表
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
}else{
// 单链表迁移
while(e.next !=null){
// hashmap的老数据 迁移后 桶下标 = 旧下标或者 (旧下标+扩容长度)
if(e.hash & oldCap == 0){
table[i] = oldTable[i]; //此时全局变量table已指向新表
}else{
table[i] = oldTable[i+newCap/2]
}
}
}
}
}
// todo putTreeVal 和 tree.splite 两个源码?