一篇解决Java面试中所有HashMap问题

在这里插入图片描述

1.不同版本JDK中HashMap的变化

1.新的节点插入链表。JDK8之前是头插法,后来是尾插法。
2.如果同一个格子里的key不超过8个,使用链表结构存储。
如果超过了8个,那么会调用treeifyBin函数,将链表转换为红黑树。
那么即使hashcode完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(log n)的开销,也就是说put/get的操作的时间复杂度最差只有O(log n)
3.在JDK1.7的时候是先进行扩容后进行插入,而在JDK1.8的时候则是先插入后进行扩容
4.JDK1.7时,用位运算公式。而JDK1.8使用JDK1.7的规律,具体如图。在这里插入图片描述

2.为什么使用尾插法

数组容量是有限的,数据多次插入的,到达一定的数量就会进行扩容,也就是resize。
扩容步骤:首先创建一个新的Entry空数组,长度是原数组的2倍;然后遍历原Entry数组,把所有的Entry重新Hash到新数组。
resize的赋值方式,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。
在这里插入图片描述

Q:为什么要rehash?
A:是因为长度扩大以后,Hash的规则也随之改变。
index = HashCode(Key) & (Length - 1)

3.HashMap是线程安全的吗

不是。put/get方法都没有加同步锁,多线程情况最容易出现的就是:无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全还是无法保证。
在这里插入图片描述

4.HashMap的默认初始长度为什么是16

最好是2的幂。
这样是为了位运算的方便,&运算比算数计算的效率高了很多,之所以选择16,是为了服务将Key映射到index的算法。
index的计算公式:index = HashCode(Key) & (Length- 1)

当Length=16时,Lengh-1=15D=1111B
HashCode&1111等价于HashCode%16

Q:为什么是16不是别的?
A:因为在使用不是2的幂的数字的时候,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。
只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。这是为了实现均匀分布。

5.HashMap指定容量初始化会发生什么

HashMap并不一定会直接采用我们传入的数值,而是经过计算,得到一个新值,目的是提高hash的效率。(1->1、3->4、7->8、9->16)。也就是找到第一个不小于它的2的幂。

6.HashMap的扩容

扩容条件
当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。
临界值threshold
threshold = loadFactor * capacity。
capacity
容量,2的幂
loadFactor
装载因子,默认是0.75
好处
0.75正好是3/4,而capacity又是2的幂。所以,两个数的乘积是整数。
扩容后大小
原来的2倍

if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
    newThr = oldThr << 1; // double threshold
}

7.HashMap中的链表+红黑树

在jdk1.8中

Q:为什么会选择8作为链表转红黑树的阈值?
A:根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。

8.HashMap和Hashtable的区别

1.HashMap线程不安全,Hashtable线程安全。
2.HashMap能够将键设为null,也可以将值设为null。Hashtable不能将键和值设为null,否则运行时会报空指针异常错误。

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

Q:为啥Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null?
A:这是因为Hashtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据。
如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理。

3.实现方式不同:Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类。
4.初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。
5.扩容机制不同:当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。
6.迭代器不同:HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的。
所以,当其他线程改变了HashMap 的结构,如:增加、删除元素,将会抛ConcurrentModificationException 异常,而 Hashtable 则不会。

8.1 fail-fast

介绍

快速失败(fail-fast)是Java集合中的一种机制,在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除修改),则会抛出ConCurrent Modification Exception。

原理

迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个modCount变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。
每当迭代器使用hasNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount的值,是的话就返回遍历;否则抛出异常,终止遍历。

Tip:这里异常的抛出条件是检测到 modCount!=expectedmodCount
这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。
因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。
安全失败(fail-safe),java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

9.HashSet

HashSet底层是通过HashMap实现的。

构造函数

调HashMap的

add

HashSet添加的元素是存放在HashMap的key位置上,而value取了默认常量PRESENT,是一个空对象。

private static final Object PRESENT = new Object();

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

10.遍历HashMap

参考
使用迭代器iterator()
1.HashMap.entrySet()
2.HashMap.keySet()
3.HashMap.values()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值