HashMap相关知识点和面试题

1.为什么HashMap要用数组加链表来实现?

结合数组和链表的优点:
1.查询和修改效率高
2.增删和删除效率也高
3.解决hash冲突的问题

or_FFFFFF,t_70,g_se,x_16)

2.HashMap的put方法的大致实现流程?

1. 判断数组是否为空,为空进行初始化;
2. 不为空,计算 k 的 hash 值,通过 (n - 1) & hash 计算应当存放在数组中的下标 index;
3. 查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;
4. 存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,
用新的value替换原数据(onlyIfAbsent为false)5. 如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;
(如果当前节点是树型节点证明当前已经是红黑树了) 
6. 如果不是树型节点,创建普通Node加入链表中;判断链表长度是否大于 8并且数组长度大于64,
大于的话链表转换为红黑树;
7. 插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍。

在这里插入图片描述

3.HashMap中数组的大小有什么特点?

大小是2的幂次方

4.HashMap中数组的大小为什么要是2的幂次方数?

因为2的幂-1都是11111结尾的,所以位运算的结果碰撞几率小
总结:为了减少Hash碰撞,尽量使Hash算法的结果均匀分布

5.HashMap中是如何计算数组下标的?

1.对key进行hash计算,求出hashcode
2.将hashcode的高16位和低16位进行异或操作。
3(n - 1) & hash ,将hash值与length-1进行与操作,求下标的位置

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

 if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

6.你知道HashMap的哈希函数怎么设计的吗?为什么这样设计?

hash函数是先拿到 key 的hashcode,是一个32位的int值,然后让hashcode的高16位和低16位进行异
或操作。
这么设计有二点原因:
1. 一定要尽可能降低hash碰撞,越分散越好;
2. 算法一定要尽可能高效,因为这是高频操作, 因此采用位运算;

在这里插入图片描述

7.为什么采用hashcode的高16位和低16位异或能降低hash碰撞?hash函数能不能直接用key的hashcode?

因为key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。int值范围为-2147483648~2147483647,前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。
但问题是一个40亿长度的数组,内存是放不下的。如果HashMap数组的初始大小才16,用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。

8.HashMap是如何进行扩容的?1.8和1.7有什么不一样?新元素计算下标有什么不一样?

默认大小是16,负载因子是0.75, 如果自己传入初始大小k,初始化大小为大于k的2的整数次方,例如如果传10,大小为161.HashMap触发扩容条件
	1)hashMap默认的负载因子是0.75,即如果hashmap中的元素个数超过了总容量75%,则会触发扩容
	2)如果某个桶中的链表长度大于等于8了,则会判断当前的hashmap的容量是否大于64,如果小于64,则会进行扩容;如果大于64,则将链表转为红黑树
2.扩容过程
1.71.8HashMap的扩容都是每次扩容为原来的两倍,原来数组中的元素全部放到新的数组newtable

在这里插入图片描述

9.多线程情况下HashMap1.7在扩容时为什么会出现线程不安全?

A线程在插入节点BB线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插
法,后遍历到的B节点放入了头部,这样形成了环,如下图所示:

在这里插入图片描述

10.HashMap中的modcount表示什么意思?

modCount用于记录HashMap的修改次数
在HashMapput(),get(),remove(),Interator()等方法中,都使用了该属性;

11.HashMap为什么会出现ConcurrentModificationException?

Itrnext()remove()forEachRemaining()里面,它们都会校验集合是否被修改,如果有修改,则抛出ConcurrentModificationException异常。

校验的原理也很简单,就是对比当前的集合的修改次数与当时Itr对象创建时记录的次数,如果不一致,就是被修改过了。

12.1.8对hash函数做了优化,1.8还有别的优化?

1. 数组+链表改成了数组+链表或红黑树;
2. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将
新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;
3. 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断
逻辑,位置不变或索引+旧容量大小;
4. 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;

优化目的:
1.防止发生hash冲突,链表长度过长,将时间复杂度由 O(n) 降为 O(logn) ; 
2. 因为1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;

13.扩容的时候为什么1.8 不用重新hash就可以直接定位原节点在新数据的位置呢?

这是由于扩容是扩大为原数组大小的2倍,用于计算数组位置的掩码仅仅只是高位多了一个1.

怎么理解呢?
扩容前长度为16,用于计算(n-1) & hash 的二进制n-10000 1111,扩容为32后的二进制就高位多了1,为0001 1111。
因为是& 运算,1和任何数 & 都是它本身,那就分二种情况,如下图:原数据hashcode高位第4位 为0和高位为1的情况;
第四位高位为0,重新hash数值不变,第四位为1,重新hash数值比原来大16(旧数组的容量)

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

酆都小菜鬼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值