总结

首先它的底层结构是xxx,它的put方法原理是xxx。下标计算原理是xxx,当put元素达到一定值的时候会进行扩容,其扩容原理是xxx,其容量都是2的n次方,原因是xxx。
在jdk1.8之前,是头插法,1.8之后改为了尾插法,原因是xxx。
同时1.8之前和1.8之后都是线程不安全的,体现在xxx。
1.8之后还引入了红黑树,好处是xxx

1.底层结构

首先它的底层是数组+链表组成的散列表。这种能够综合数组(查找效率高)和链表(增删效率高)的优点,提高效率。

2.put

一开始数组的容量为0,HashMap会把数组长度初始化为16。当put节点的时候,会先计算这个节点的key的哈希值,然后得到节点的下标,如果此位置没有节点,则把节点放到这个下标处。如果已经有节点了,就要遍历这个节点所在的链表,看是否有key==为true或者equals方法为true的,如果有就进行value值的更新,如果没有尾插法把节点链到最后

3.key的下标/hash值是怎么计算的

具体方法是HashMap里面的hash方法,它会调用key的hashCode方法,然后将得到的哈希值,高16位和低16位进行异或运算,得到key的哈希值。然后再将此哈希值与数组长度取余(虽然是&符号,但是效果和取余效果是一样的,只是效率要比%符号高),得到下标

方法一:
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); //第三步 取模运算
}

哈希值的计算:将对象的hashcode()方法返回的hash值,进行无符号的右移16位,并与原来的hash值进行按位异或操作,目的是将hash的低16bit和高16bit做了一个异或,使返回的值足够散列
下标的计算:将哈希值与(数组长度-1)取余,得到下标。用&的作用是效率更高

4.扩容机制是什么?

当容量达到size*0.75的时候,会进行扩容。HashMap扩容的方法是使用一个新的数组代替原有的数组,对原数组的所有数据进行重新计算插入新数组,之后指向新数组;如果扩容前数组已经达到最大了,那么将直接将阈值设置成最大整数,然后return;

5.​​头插法还是尾插法,优点是啥​

JDK1.8之前是头插法,1.8之后是尾插法
在并发环境下,扩容时,如果是头插法,会造成死循环。而且热点数据在扩容后会倒置,更不需要头插法了

6.红黑树

JDK1.8之后引入红黑树,数组大小大于64且链表长度大于8的时候会转成红黑树,小于64只会进行扩容。链表长度为6的时候又转回链表。红黑树是非严格平衡的二叉树,这样能够提高检索效率。

7.为啥是2的n次方

在计算buckets桶位置的时候,公式为​​((n-1) & hash)​​,2的幂减去1的数的二进制数的结尾都是1,与hash值进行与运算,会得到其余数。进行按位与操作,使得结果剩下的值为对象的hash值的末尾几位,这样就我们只要保证对象的hash值生成足够散列即可
总之就是,使得​​​((n-1) & hash)​​计算的索引值更均匀,能尽量减少碰撞。

8.​​线程不安全是影响的哪里​

在jdk1.8之前,扩容(transfer方法)的时候容易形成死链和数据丢失,1.8之后,也是线程不安全的,会因为size造成数据覆盖的情况