HashMap
🚀面试口诉
1,hashmap是我们几乎每天用到的集合类,它以键值对的形式存在。
2, 在jdk1.7中:底层是数组加链表,1.8中采用的是数组加链表加红黑树,红黑树的引入是为了提高查询效率
3,1.7中hash碰撞采用头插法,头插法会形成循环链表(?),1.8尾插法
4,hash算法1.8进行了简化,up主记不清了,
5,最好传入一个二的次幂的初始化容量, put时,会初始化数组,容量为大于等于初始化容量的最近的二次幂,比如初始化容量为6,他初始化就是8。
6,key的hash值 与 上容量-1,计算出所在位置
7,扩容的问题:加载因子0.75,达到 容量 *0.75后就会扩容,两倍扩容
8,树化,数组容量达到64,链表长度大于等于8,后才会进行树化,链表长度小于6就会解除树化
构造方法:
put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
HashMap存储位置计算
jdk1.7
位置计算方式
key.hashCode()&(length-1)
jdk1.8高低索引
原因:HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同
//jdk 1.80引入了高低索引,使得更加随机
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
位置计算方式
hash(k)&(length-1)
假设下标直接是key.hashCode()&(length-1),则只有hashCode()的低位参与运算(因为hashCode()是32位的,而length-1一般不大,比如容量16,16-1=15,15二进制就是1111,换成32位的话,高位就全是0了,根据&运算规则1&1=1,其他情况都是0,所以hashCode()高位没参与运算了),如果低位不变,只是高位变,key.hashCode()&(length-1)值还是一样,那么就冲突了。
key.hashCode()右移16位就是高位变成了低位,原来高位全补0,,然后做^运算,实际就是原来的hashCode()值的高位与低位做^运算,这就解决了低位不变高位无论变与不变仍冲突的问题,现在无论高位低位哪个变,最终hash值都会变,更加散列
树化
树化前提:
- 容量大于64
- 链表长度大于8
过程:
- 把普通节点转化为treeNode
- 调用treeify进行树化
树化消耗性能,减少树化
扩容
前提
- size大于等于 容量*加载因子
扩容大小
原本来的两倍,左移一位
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
扩容机制
计算hash值在旧容量最高位对应的二进制是1还是0,是1则会移动到高位索引(原索引位置+原容量),是零则在低位索引也就是原位置
get方法
总结
添加新数据的方式
1.7头插
优势:
- 头插不需要遍历
- 设计者认为后插入的一般会先查找
问题:
- 多线程有可能循环链表产生,找不到尾节点,cup卡死
1.8尾插