HashMap 常见面试题总结

HashMap 常见面试题总结

1、HashMap 底层原理

(1)HashMap的实现采用了除留余数法形式的哈希函数和链地址法解决哈希地址冲突的方案。这样就涉及到两种基本的数据结构:数组和链表。数组的索引就是对应的哈希地址,存放的是链表的头结点即插入链表中的最后一个元素,链表存放的是哈希地址冲突的不同记录。
(2)使用 HashMap put 元素时,先根据 key 的 hash 值得到这个 Entry 元素在数组中的位置(即下标),然后把这个 Entry 元素放到对应的位置中,如果这个 Entry 元素所在的位子上已经存放有其他元素就在同一个位子上的 Entry 元素以链表的形式存放,新加入的放在链头
(3)使用 HashMap get Entry 元素时先计算 key 的 hashcode,找到数组中对应位置的某一 Entry 元素,然后通过 key 的 equals 方法在对应位置的链表中找到需要的 Entry 元素

2、HashMap 为什么设计成数组+链表式结构

(1)由于数组存储区间是连续的,占用内存严重,故空间复杂度大,查找时间复杂度小(O(1)),所以寻址容易而插入和删除困难
(2)而链表存储区间离散,占用内存比较宽松,故空间复杂度小,但时间复杂度大(O(N)),所以寻址困难而插入和删除容易
(3)所以就产生了一种新的数据结构叫做哈希表,哈希表既满足数据的查找方便,同时不占用太多的内容空间,使用也十分方便,哈希表有多种不同的实现方法,HashMap 采用的是链表的数组实现方式

3、为什么默认长度和扩容后的长度都必须是 2 的幂

先来了解一下put()方法的部分源码:

public V put(K key, V value) {
 
   ......
 
   //计算出 key 的 hash 值
   int hash = hash(key);
   //通过 key 的 hash 值和当前动态数组的长度求当前 key 的 Entry 在数组中的下标
   int i = indexFor(hash, table.length);
}
//最核心的求数组下标方法
static int indexFor(int h, int length) {

   // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
   return h & (length-1);
}

重点是这一句:

 return h & (length-1);

可以看出,key的hash值 与 (数组长度-1)后的与运算 就是存储Entry的数组下标。
(1)先来了解一下与运算,两个同时为1,结果为1,否则为0,而且与运算 的效率远比取模运算好

(2)当数组长度为2的n次幂时,转成二进制的话,那么它的所有位都为1,与hash值 与运算 后的值就是hash值后n位的值
例如:(11101010) & 1111 = 1010
(11011111) & 1111 = 1111

(3)假如数组长度可以随意,那么假如数组容量为11时,减一后化为二进制为1010,这时经过与运算后:
(11101010) & 1010 = 1010
(11011111) & 1010 = 1010
可以看到,会发生hash 碰撞

(4)只要存入 HashMap 的 Entry 的 key 的 hashCode 值分布均匀,HashMap 中数组 Entry 元素的分部也就尽可能是均匀的(也就避免了 hash 碰撞带来的性能问题

总结:
(1)与运算 效率高
(2)可以减少避免hash 碰撞带来的性能问题

4、HashMap和Hashtable有主要区别

(1)继承关系不同
HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口;

(2)Hashtable比HashMap多提供了elments() 和contains() 两个方法;

(3)线程安全性不同
HashMap的方法都没有使用synchronized关键字修饰,都是非线程安全的,而Hashtable的方法几乎,都是被synchronized关键字修饰的,是线程安全的;

(4)key-value支持类型不同
HashMap: key-value,null-null,key-null,null-value
HashTable: 只支持key-value

既然HashMap支持带有null的形式,那么在HashMap中不能由get()方法来判断
HashMap中是否存在某个键, 而应该用containsKey()方法来判断
,因为使用get的时候,当返回null时,你无法判断到底是不存在这个key,还是这个key就是null,还是key存在但value是null。

(5)初始容量大小和每次扩充容量大小的不同
Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。

(6)计算hash值的方法不同
Hashtable在计算元素的位置时需要进行一次除法运算,而除法运算是比较耗时的。
HashMap为了提高计算效率,将哈希表的大小固定为了2的幂,这样在取模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。当然HashMap的效率虽然提高了,但是hash冲突却也增加了。

5、关于HashMap 构造方法中 initialCapacity(初始容量)、loadFactor(加载因子)的理解

这两个参数对于 HashMap 来说很重要,直接从一定程度决定了 HashMap 的性能问题
(1)initialCapacity 初始容量代表了哈希表中桶的初始数量,即 Entry< K,V>[] table 数组的初始长度,可以会通过 roundUpToPowerOf2(initialCapacity) 方法来保证为 2 的幂次;

(2)加载因子
1、加载因子 = map中Entry 的数量 / 数组长度;
2、是哈希表在其容量自动增加之前可以达到多满的一种饱和度百分比,其衡量了一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小;
3、当哈希表中 Entry 的数量超过了 loadFactor 加载因子乘以当前 table 数组桶长度时就会触发扩容操作;
4、如果负载因子越大则对空间的利用更充分,从而导致查找效率的降低,如果负载因子太小则散列表的数据将过于稀疏,从而对空间造成浪费。系统默认负载因子为 0.75,一般情况下无需修改

6、 JDK1.7 中 HashMap 什么情况下会发生扩容?如何扩容?

看一下扩容的核心代码

//JDK1.7扩容最核心的方法,newTable为新容量数组大小
void transfer(HashMapEntry[] newTable) {
 
   //新容量数组桶大小为旧的table的2倍
   int newCapacity = newTable.length;
 
   //遍历旧的数组桶table
   for (HashMapEntry<K,V> e : table) {

       //如果这个数组位置上有元素且存在哈希冲突的链表结构则继续遍历链
       while(null != e) {
        
           //取当前数组索引位上单向链表的下一个元素
           HashMapEntry<K,V> next = e.next;
 
           //重新依据hash值计算元素在扩容后数组中的索引位置
           int i = indexFor(e.hash, newCapacity);
 
           //将数组i的元素赋值给当前链表元素的下一个节点
           e.next = newTable[i];
 
           //将链表元素放入数组位置
           newTable[i] = e;
 
           //将当前数组索引位上单向链表的下一个元素赋值给e进行新的一圈链表遍历
           e = next;
 
       }
   } 
}

1、何时发生扩容
当哈希表中 Entry 的数量超过了 loadFactor 加载因子乘以当前 table 数组桶长度时就会触发扩容操作;

2、扩容的步骤
(1)新建大小为扩容后大小的新数组;
(2)取出数组元素,然后遍历以该元素为头的单向链表元素,依据每个被遍历元素的 hash 值计算其在新数组中的下标然后进行交换即可(扩容后,原来 hash 冲突的单向链表尾部变成了扩容后单向链表的头部)

7、JDK 1.8 中 HashMap 是如何扩容的?与 JDK 1.7 有什么区别

两个版本的扩容步骤几乎一样,但1.8 引进了红黑树,所以遇到树需要做相对应的处理。
区别:
(1)优化点:由于扩容数组的长度是 2 倍关系,所以对于假设初始 tableSize =4 要扩容到 8 来说就是 0100 到 1000 的变化(左移一位就是 2 倍),在扩容中只用判断原来的 hash 值与左移动的一位按位与操作是 0 或 1 就行,0 的话索引就不变,1 的话索引变成原索引加上扩容前数组
(2)在 JDK1.7 中扩容操作时哈西冲突的数组索引处的旧链表元素扩容到新数组时如果扩容后索引位置在新数组的索引位置与原数组中索引位置相同,则链表元素会发生倒置;而在 JDK1.8 中不会出现链表倒置现象

8、HashMap在jdk1.8为何引入了红黑树

红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。加快检索速率

发布了21 篇原创文章 · 获赞 11 · 访问量 781

猜你喜欢

转载自blog.csdn.net/qqq3117004957/article/details/104536838