signed

QiShunwang

“诚信为本、客户至上”

集合(关于HashMap)

2021/4/26 16:20:17   来源:

关于HashMap

1)关于常见的集合简述?

答:Map接口和Collection接口是所有集合框架的父接口:

Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等

Collection接口的子接口包括:Set接口和List接口

Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等

List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

 

2)HashMap与HashTable的区别?

(初始默认值,线程安全,NULL,源文件)

HashTable默认初始容量为11 HashMap的是16

HashMap没有考虑同步,是线程不安全的;Hashtable使用了synchronized关键字,是线程安全的;

HashMap允许K/V都为null;后者K/V都不允许为null;

HashMap继承自AbstractMap类;而Hashtable继承自Dictionary类;

 

3)HashMap的put方法的具体流程?

1.首先判断table数组是否为空长度是否为空,为空时使用resize()方法扩容

2.对插入的数组索引i进行计算,计算方法和1.7中的indexFor方法一样;如果数组为空,即不存在hash冲突,直接插入数组;

3.插入时,如果发生hash冲突,则依次往下判断

a.判断table[i]的元素的key是否与需要插入的key一样,若相同则直接用新的value覆盖掉旧的value

判断原则equals() - 所以需要当key的对象重写该方法

 b.继续判断:需要插入的数据结构是红黑树还是链表        

如果是红黑树,则直接在树中插入 or 更新键值对

如果是链表,则在链表中插入 or 更新键值对

   i .遍历table[i],判断key是否已存在:采用equals对比当前遍历结点的key与需要插入数据的key            

  如果存在相同的,则直接覆盖   

  ii.遍历完毕后仍然发现上述情况,则直接在链表尾部插入数据

 插入完成后判断链表长度是否> 8:若是,则把链表转换成红黑树

对于i 情况的后续操作:发现key已存在,直接用新value覆盖旧value&返回旧value

插入成功后,判断实际存在的键值对数量size是否 > 最大容量  

如果大于则进行扩容

 

4)HashMap的resize()?

答:该函数有2种使用情况:1.初始化哈希表;2.当前数组容量过小,需要扩容

针对情况2:若扩容前的数组容量超过最大值/键值对个数,则不再扩容

针对情况2:若没有超过最大值/键值对个数,就扩容为原来的2倍(左移1位)

针对情况1:初始化哈希表(采用指定或者使用默认值的方式)

将初始容量放置在阈值中

零初始阈值表示使用默认值

 

5)HashMap是怎么解决哈希冲突的? 

答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希才行;

 

什么是哈希?

Hash,“散列”,“哈希”,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);

所有散列函数都有如下一个基本特性:

根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。

根据同一散列函数计算出的散列值如果相同,输入值不一定相同(可能相同)。

 

什么是哈希冲突?

当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。

 

HashMap的数据结构

在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突:

 

 

这样我们就可以将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化

 

hash()函数

上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下:

static final int hash(Object key) {

    int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

// 与自己右移16位进行异或运算(高低位异或)

}

这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动);

 

JDK1.8新增红黑树

 

 

通过上面的链地址法(使用散列表)和扰动函数我们成功让我们的数据分布更平均,哈希碰撞减少,但是当我们的HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn);

 

 

 

6)HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

答:hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;

 

MSG:那怎么解决呢?

答:

HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;

保证数组长度为2的幂/n次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂/n次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;

 

MSG:为什么数组长度要保证为2的幂/n次方呢

答:

只有当数组长度为2的幂/n次方时,h&(length-1)才等价于h%length,即实现了key的定位,2的幂次方也可以减少冲突次数,提高HashMap的查询效率;

如果 length 为 2 的幂/n次方 则 length-1 转化为二进制必定是 11111……的形式,在于 h 的二进制与操作效率会非常的快,而且空间不浪费;如果 length 不是 2 的幂/n次方,比如 length 为 15,则 length - 1 为 14,对应的二进制为 1110,在于 h 与操作,最后一位都为 0 ,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。

 

MSG:那为什么是两次扰动呢?

答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的;

 

7)HashMap在JDK1.7和JDK1.8中有哪些不同?

答:

JDK 1.7存储结构 数组 + 链表

JDK 1.8存储结构 数组 + 链表 + 红黑树

初始化方式单独函数:inflateTable()直接集成到了扩容函数resize()中
hash值计算方式

扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算

扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算

存放数据的规则

无冲突时,存放数组;冲突时,存放链表

无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树插入数据方式头插法(先讲原位置的数据移到后1位,再插入数据到该位置)尾插法(直接插入到链表尾部/红黑树)

 

8)为什么HashMap中String、Integer这样的包装类适合作为K?

答:

String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率

 

1.都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况

2.内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;

 

MSG:如果我想要让自己的Object作为K应该怎么办呢?

答:重写hashCode()和equals()方法

1.重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;

2.重写`equals()`方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性