微前端架构如何改变企业的开发模式与效率提升
669
2022-11-10
JDK1.8 HashMap源码解析+最全面试题
目录
HashMap简介一、源码分析
1. 成员变量
1)面试题1:为了链表转红黑树的阈值是8?2)面试题2:为什么负载因子默认是0.75?3)面试题3:为什么HashMap的容量是2的n次幂?4)面试题4:什么时候链表会转为红黑树(细节坑)?5)面试题5:HashMap什么时候扩容?如何扩容?
2.构造方法3.put()方法
1)面试题6:HashMap中的数组什么时候初始化?2)面试题7:多线程环境下,put方法存在哪些问题?3)面试题8:HashMap工作原理?4)面试题9:当两个对象的hashcode相同会发生什么?5)面试题10:HashMap的hash是如何实现的?
4. get()方法5. resize()方法6. remove()方法
常见其他面试题
1)面试题11:HashMap的几种遍历方式?2)面试题12:HashMap、LinkedHashMap、TreeMap的区别和使用场景?3)面试题13:HashMap和HashTable的区别?
HashMap简介
HashMap底层数据结构采用数组+ 链表 + 红黑树。数组是HashMap的主体,链表是为了解决Hash冲突的问题、红黑树是为了解决Hash冲突达到一定程度后影响查询效率问题。其通过散列映射来存储键值对数据,所以根据key的哈希值取模数组长度可以以O(1)的时间复杂度快速查询到键值对数据。另外,它是非线程安全的,并且因为散列hash的缘故,它是无序的。
HashMap继承于AbstractMap,实现了Map、cloneable、Serializable接口;是线程不安全的;key、value允许为null,但只允许一个key值为null
一、源码分析
1. 成员变量
/** * 初始容量默认为16,并且必须为2的n次幂. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; /** * 最大容量,MUST be a power of two <= 1<<30. */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 默认负载因子 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 链表转红黑树阈值 */ static final int TREEIFY_THRESHOLD = 8; /** * 红黑树转链表阈值. */ static final int UNTREEIFY_THRESHOLD = 6; /** * 链表想转红黑树的前提必须是HashMap的table数组容量大于64,否者table两倍扩容 */ static final int MIN_TREEIFY_CAPACITY = 64;/** * HashMap的主体:数组,容量可以为0,为了允许当前不需要的引导机制 */ transient Node
1)面试题1:为了链表转红黑树的阈值是8?
因为链表中的节点遵循泊松分布,并且链接表的节点数是8的概率接近千分之一,此时链表的查询性能已经很差了。在这种情况下,将链表转为红黑树可以挽回分布查询性能,而树节点所占空间是普通节点的二倍也不再是主要矛盾了。
2)面试题2:为什么负载因子默认是0.75?
负载因子表示一个散列表的空间使用程度,HashMap的容量 = capacity * loadFactory。loadFactory的默认值是0.75是为了在时间和空间上追求一种折中。
如果负载因子过高,虽然会扩大散列表的装填程度,使HashMap能容纳更多的元素;但是Hash碰撞的概率也会更高。如果负载因子过低,虽然会降低hash碰撞的概率,提高数据检索的效率;但会造成空间上的浪费。
3)面试题3:为什么HashMap的容量是2的n次幂?
提高与运算的效率。与运算的效率高于取模运算,计算数组容量为2^n的下标时,与运算和取模运算的结果一样。并且与运算可以减少hash碰撞的概率。进行数据迁移时,可以快速定位到索引位置。
顺带一提扰动函数:为了减少Hash碰撞的概率,尽量让不同的key对应不同的hash值。在hash()中的高16位与低16位异或就可以理解为是一种扰动函数。
4)面试题4:什么时候链表会转为红黑树(细节坑)?
这里说白了就是为了看你是否真的看过源码,因为HashMap中有个MIN_TREEIFY_CAPACITY变量,表示允许执行treeifyBin()操作HashMap的最小元素个数。 所以我们只答链表的长度达到8是不全对的,还要说出元素数量大于64。
5)面试题5:HashMap什么时候扩容?如何扩容?
当Map的元素个数size超过threshold(capacity * loadFactory)时,进行扩容。 扩容的时候:
会先创建一个原HashMap两倍大小的Node数组,然后将原Node数组的元素rehash到新的Node数组。键值对的数组位置位置有可能是原数组下标位置、可有可能是原下标+原容量的位置。也就是原本只取key-hash值的低四位,扩容后取第五位,然后与0X7FFFFF取模。取模后如果第五位是1,下标就在原下标+原容量的位置。扩容时,JDK1.7采用头插法;JDK1.8之后采用尾插法。
2.构造方法
/** * 设置初始容量和加载因子 */ public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } /** * 设置初始容量,并使用默认的加载因子 */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * 空参构造函数,使用默认的加载因子0.75 */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } /** * Constructs a new HashMap with the same mappings as the * specified Map. The HashMap is created with * default load factor (0.75) and an initial capacity sufficient to * hold the mappings in the specified Map. * * @param m the map whose mappings are to be placed in this map * @throws NullPointerException if the specified map is null */ public HashMap(Map extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
我们着重看一下最后一个构造函数:通过接收map对象进行初始化。
final void putMapEntries(Map extends K, ? extends V> m, boolean evict) { // 获取Map的size int s = m.size(); if (s > 0) { // 数组还未初始化。 if (table == null) { // pre-size // Map的Capacity,为数组容量 / loadFactor , +1是为了向上取整,因为除法很难得到整数。 float ft = ((float)s / loadFactor) + 1.0F; // 计算最大容量 int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) // 容量转为2的n次幂 threshold = tableSizeFor(t); } // 大于扩容阈值则扩容 else if (s > threshold) resize(); // 将m中的数据复制到HashMap中。 for (Map.Entry extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
3.put()方法
HashMap的核心基本都体现在put方法,从put方法我们可以理解到HashMap的工作原理。
put方法流程图如下:
在聊put方法之前,我们先看看hash方法,HashMap是怎么计算key的哈希值的。
static final int hash(Object key) { int h; // 使用到了上述提到的扰动函数,高十六位与低十六位异或 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
下面来看看put()方法:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * put()方法真正逻辑所在 * * @param hash key的hash值 * @param key key * @param value value值 * @param onlyIfAbsent true表示不更新现有值 * @param evict 如果为false,则表处于创建模式。HashMap中暂未使用 */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // tab 哈希数组,p 该哈希桶的首节点,n hashMap的长度,i 计算出的数组下标 Node
1)面试题6:HashMap中的数组什么时候初始化?
从HashMap的构造方法中我们可以看到,它只是指定一些成员变量(使用Map对象的构造方法除外)。 所以可以这样聊:
正常情况下,我们使用HashMap的构造方法之后只是指定一些成员变量、比如loadFactor、threshold。真正的初始化发生在给Map添加第一个元素时。而使用map参数的构造方法时,初始化发生在构造方法中。但是本质上还是通过putVal()方法添加元素对数组进行初始化。
2)面试题7:多线程环境下,put方法存在哪些问题?
1、数据被覆盖
两个线程A/B同时进行put操作,并且hash函数计算出要插入数组下标相同时,A线程刚计算完hash值还没来得及插入数据,CPU时间片耗尽被挂起。线程B接着执行完了全部操作,然后线程A恢复,那么最终线程B插入的数据就被A覆盖了。
2、size偏小
++size操作是非原子性操作,存在线程安全问题。加入两个线程A/B同时进入++size操作,获取到原size的值一样,进行赋值操作时,它俩就等于是只给size+了1。
3)面试题8:HashMap工作原理?
HashMap基于数组+ 链表 + 红黑树的数据结构。数组是HashMap的主体,链表是为了解决Hash冲突的问题、红黑树是为了解决Hash冲突达到一定程度后影响查询效率问题。使用Node来存储键值对。通过散列映射来存储键值对数据,所以根据key的哈希值取模数组长度可以以O(1)的时间复杂度快速查询到键值对数据。接着聊一下负载因子的作用。最后讲一讲put()方法流程。其中建议提到扰动函数、rehash等词汇。具体话术请读者参考put()方法流程图梳理。get()、remove()方法捎带一提。
4)面试题9:当两个对象的hashcode相同会发生什么?
发生hash碰撞因为hashCode相同,所以它们的bucket位置相同,‘碰撞’就发生了。因为HashMap使用链表存储对象,这个Node对象会存储在链表中。当链表长度达到8时,链表将转为红黑树。
5)面试题10:HashMap的hash是如何实现的?
JDK1.8中,在hash()方法将hashCode的高16位与低16位做异或操作。我们可以把它看做是一种扰动函数,hashCode的高16位和低16位都参与了数组下标的计算,减少了hash碰撞的概率。
4. get()方法
获取数据流程如下:
public V get(Object key) { Node
5. resize()方法
HashMap扩容逻辑:
final Node
6. remove()方法
public V remove(Object key) { //临时变量 Node
常见其他面试题
1)面试题11:HashMap的几种遍历方式?
map.keySet() – 建议在只需要key值时使用。
for (String key : map.keySet()) { map.get(key);}
map.values() – 建议在需要使用value值时使用。
for (String value:map.values()){ System.out.println("value:"+value);}
map.entrySet() – 建议在需要同时使用k-v时使用。
Set
map.entrySet().iterator() – 迭代器方式,需要对map的元素进行remove操作时。
Map
2)面试题12:HashMap、LinkedHashMap、TreeMap的区别和使用场景?
一般情况下HashMap当做Hash散列表使用。在 Map 中进行插入、删除和定位元素操作时,使用HashMap。TreeMap 实现 SortMap 接口,能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器)在需要按自然顺序或自定义顺序遍历键的情况下使用TreeMap。LinkedHashMap 保存了记录的插入顺序,在用 Iterator 遍历时,先取到的记录肯定是先插入的;遍历比 HashMap 慢;在需要输出的顺序和输入的顺序相同的情况下使用LinkedHashMap。
3)面试题13:HashMap和HashTable的区别?
HashMap允许key和value为null,Hashtable不允许。HashTable在写入时会比较慢。HashMap速度比较快。Hashtable是线程安全的,HashMap不是的。所以HashMap适用于单线程,HashTable多线程。Hashtable的初始容量为11,扩容为原来的2n+1;HashMap的初始容量为16,扩容为2n。HashMap提供了对key的Set进行遍历,因此HashMap是fast-fail的。HashTable提供了对key的Enumeration(列举)进行遍历,它不支持fast-fail。Hashtable被认为是一个遗留的类,想要在迭代的时候修改Map,应该使用ConcurrentHashMap。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~