1.为什么HashMap要用数组加链表来实现?
结合数组和链表的优点:
1.查询和修改效率高
2.增删和删除效率也高
3.解决hash冲突的问题
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,大小为16。
1.HashMap触发扩容条件
1)hashMap默认的负载因子是0.75,即如果hashmap中的元素个数超过了总容量75%,则会触发扩容
2)如果某个桶中的链表长度大于等于8了,则会判断当前的hashmap的容量是否大于64,如果小于64,则会进行扩容;如果大于64,则将链表转为红黑树
2.扩容过程
1.7和1.8:HashMap的扩容都是每次扩容为原来的两倍,原来数组中的元素全部放到新的数组newtable
9.多线程情况下HashMap1.7在扩容时为什么会出现线程不安全?
A线程在插入节点B,B线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插
法,后遍历到的B节点放入了头部,这样形成了环,如下图所示:
10.HashMap中的modcount表示什么意思?
modCount用于记录HashMap的修改次数
在HashMap的put(),get(),remove(),Interator()等方法中,都使用了该属性;
11.HashMap为什么会出现ConcurrentModificationException?
在Itr的next()、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-1为0000 1111,扩容为32后的二进制就高位多了1,为0001 1111。
因为是& 运算,1和任何数 & 都是它本身,那就分二种情况,如下图:原数据hashcode高位第4位 为0和高位为1的情况;
第四位高位为0,重新hash数值不变,第四位为1,重新hash数值比原来大16(旧数组的容量)