面经-HashMap
应用场景:快速查找
通过计算元素的哈希码,得到元素的桶下标,计算元素位置。接下来只需要进行少量的比较即可找到元素。
当链表长度过长时,可以通过扩容减少链表长度。但如果元素的原始哈希码都一样时,即使扩容也无法改变链表长度,只能进行红黑树的树化。
底层数据结构,1.7与1.8有何不同?
1.7 数组+链表
1.8 数组+(链表|红黑树)
为什么要用红黑树?
红黑树:父节点左边都是比它小的元素;父节点右边都是比它大的元素。
数组链表过长会影响HashMap的性能。引入红黑树后链表过长也不会影响性能。
何时会树化?
1.链表长度大于8.
2.数组容量大于等于64.如果不够大会首先尝试扩容。
为何一上来不树化?
链表短时性能比红黑树好。树化会占用更多内存,没必要进行树化。
树化阈值为何是8?
正常情况下链表不会超过8.遭受到恶意攻击后链表长度才会过长。
1.红黑树用来避免Dos攻击,防止链表过长时性能下降,是偶然现象。hash表时间复杂度为O(1),且Node占用空间小;而红黑树时间复杂度为O(log₂n),且TreeNode占用空间较大。非必要时尽量使用链表。
2.hash值完全随机时,在hash表内按泊松分布。在负载因子为0.75的情况下,长度超过8的链表出现的概率为亿分之6.选择8使树化的几率足够小。
何时会退化为链表?
1.扩容时拆分树,树元素<=6时退化链表。
2.remove树节点,若root(根节点)、root.left(左孩子)、root.right(右孩子)、root.left.left(左孙子)有一个为null,也会退化链表。
索引如何计算?
key——>哈希——>二次哈希——>二次哈希%数组长度 / 二次哈希 &(按位与) (数组长度-1) 得到桶下标。
HashCode都有了,为何还要提供Hash()方法?
二次哈希可以降低哈希碰撞的概率,使哈希分布更为均匀。
数组容量为何是2的n次幂?
1.hashMap是懒惰创建数组,首次使用才创建数组。
2.计算索引(桶下标)。
3.如果桶下标还没人占用,创建Node占位返回。
4.如果桶下标已经有人占用:
如果已经是TreeNode走红黑树的添加或更新逻辑;
如果是普通Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑。
5.返回前检测容量是否超过阈值,一旦超过就进行扩容。
1.7和1.8的put流程有何不同?
1.8链表插入节点时采用尾插法,新的元素插入到原来链表的尾部。1.7采用头插法。
1.7是大于等于阈值且没有空位时才扩容,而1.8是大于阈值就扩容。
1.8在扩容计算Node索引时,会优化。
加载因子为何是0.75f
在空间占用和查询时间之间取得较好的平衡。
大于这个值,空间节省了,但链表就会比较长,影响性能。
小于这个值,冲突减少了,但扩容就会更频繁,空间占用多。
多线程下操作HashMap会有什么问题?
线程不安全,会造成数据错乱。先执行的数据可能被后面的数据覆盖掉,造成并发丢失现象。
1.8:数据错乱
key能否为null?作为key的对象有什么要求?
HashMap的key可以为null。但其他Map(HashTable,TreeMap等)都不能为null,否则会出现空指针异常。
作为key的对象,必须重写HashCode和equals方法,并且key的内容不能修改(不可变)。
String对象的HashCode是怎样设计的?为何每次乘的都是31?
目的是达到较为均匀的散列效果,使每个字符串的hashCode足够独特。并且31的乘法操作可以优化为效率更高的移位和减法算法。
1.字符串中的每个字符都能表现为一个数字,称为Si,其中i的范围是0~n-1.
3.31 代入公式有较好的散列特性,并且 31 * h 可以被优化为
o 即 32 ∗h -h
o 即 2^5 ∗h -h
o 即 h≪5 -h