我们通过面试中常见的几个问题来谈谈
描述一下putVal的过程
- 数据结构:数组加链表加红黑树
- 如何确定添加的元素在底层数组的哪个位置? tab[i = (n - 1) & hash] n就是数组长度
- HashMap初始化大小是多少,为什么是16?
- 出现冲突了怎么处理?
- 怎么扩容?
- 为什么2倍扩容
- 为什么要进行树化?
hash
table
putVal
第一次使用HashMap添加数据的时候底层会创建一个长度为16的默认Node数组。
- 为啥初始化大小是16 ?
- 为什么容量要是 2 的整数次幂?
计算下标
- i = (n - 1) & hash
- n就是数组长度
- 这里是做位与运算
- 为什么要进行取模运算以及位运算
- 比如这里默认是一个长度为16的Node数组,
我们现在要根据传进来的key计算一个下标值出来然后把value放入到正确的位置,
想一下,我们用key的hashcode与数组长度做取模运算,
得到的下标值是不是一定在数组的长度范围之内,
也就是得到的下标值不会出现越界的情况。
- 比如这里默认是一个长度为16的Node数组,
- 为啥不是取模运算而是位与运算呢?
- 使用位与运算的一方面原因就是它的性能比较好,
另外一点就是这里有这么一个等式:(n - 1) & hash = n % hash
- 使用位与运算的一方面原因就是它的性能比较好,
- 为什么要减一做位运算
- 这里的n-1是为了实现与取模运算相同的效果
- 2的整数次幂减一得到的数非常特殊
- 这样得到的下标值就是均匀分布的啊,那冲突的几率就减少啦
为什么使用尾插法?
- 使用头插法会改变链表的顺序
如果扩容的话,由于原本链表顺序有所改变,扩容之后重新hash,
可能导致的情况就是扩容转移后前后链表顺序倒置,
在转移过程中修改了原来链表中节点的引用关系。 - 因为扩容前后链表顺序是不变的,他们之间的引用关系也是不变的。
扩容
- 首先是创建一个新的数组,容量是原来的二倍
- 然后会经过重新hash,把原来的数据放到新的数组上,
至于为啥要重新hash,那必须啊,你容量变了,相应的hash算法规则也就变了,得到的结果自然不一样了。
链表与树的转化
- 在Java8之前是没有红黑树的实现的,在jdk1.8中加入了红黑树,
就是当链表长度为8时会将链表转换为红黑树,
为6时又会转换成链表,这样时提高了性能,也可以防止哈希碰撞。
resize
- resize方法的主要作用就是初始化和增加表的大小,说白了就是第一次给你初始化一个Node数组,其他需要扩容的时候给你扩容
为什么不是线程安全的?
什么造成它是线程不安全的?
如何解决?
解决方案不同?
- 当多个线程同时执行 put 操作时,若键的哈希值相同,可能导致链表或红黑树节点的覆盖或丢失。
示例:两个线程同时插入不同的键值对,哈希到同一桶位置。若未同步,后一个线程的写入可能覆盖前一个线程的结果。
了解更多 java基础:目录索引