【集合】 HashMap 你知道多少 ?

最近应粉丝要求,讲一讲集合中的 HashMap, 当然 HashMap 也是面试中经常被问到的一点,如图:
在这里插入图片描述

0. HashMap 特点

  • hashmap存取是无序的
  • 键和值位置都可以是null,但是键位置只能是一个null
  • 键位置是唯一的,底层的数据结构是控制键的
  • jdk1.8前数据结构是:链表+数组;jdk1.8之后是:数组+链表+红黑树
  • 阈值(边界值)>8并且数组长度大于64,才将链表转换成红黑树,变成红黑树的目的是提高搜索速度,高效查询

1. HashMap 的底层结构?

在JDK1.7中和JDK1.8中有所区别:
在JDK1.7中,由”数组+链表“组成,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。
在JDK1.8中,有“数组+链表+红黑树”组成。当链表过长,则会严重影响HashMap的性能,红黑树搜索时间复杂度是O(logn),而链表是O(n)。因此,JDK1.8对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:

  1. 当链表超过8且数组长度(数据总量)超过64才会转为红黑树
  2. 将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。

在数组比较小时如果出现红黑树结构,反而会降低效率,而红黑树需要进行左旋右旋,变色,这些操作来保持平衡,同时数组长度小于64时,搜索时间相对要快些,总之是为了加快搜索速度,提高性能,所以数据长度大于64链表才会转红黑树。

假设数组长度为16 ,通过hash(张三)=6,根据6和16进行取模运算,结果为6,则将键值对<”张三”:"好帅">存放在该数组下标为6的位置。

map.put("张三","好帅");
map.put("李四","霸气威武");

[<>,<>,<>,<>,<>,<>,<”张三”:“好帅”>,<>,<>,<”李四”:“霸气威武”>,<>,<>,<>,<>,<>,<>]

2. HashMap 是如何解决 hash 碰撞的?

数组长度总会有限的,所以hash冲突(hash 值相同)肯定是避免不了的,所以出现了数组+链表的形式。
在这里插入图片描述
此时不难发现,当链表的长度变为 n 的时候,时间复杂度变为 O(n),所以我们可以优化链表,当链表达一定长度(默认为8)的时候,链表自动转为红黑树,此时时间复杂度变为O(logn)。

3. jdk1.8 中是如何对 hash 算法和寻址算法进行优化的?

// jdk 1.8 部分源码
 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

hash 对 数组的取模的效果与 hash &(n-1)的效果一样,都是计算,但是与运算的性能更高一些。
在这里插入图片描述
然后将 774889 所对应的二进制进行右移16位,得到的结果为 0000 0000 0000 0000 0000 0000 0000 1011 也就是将原来的高16位0000 0000 0000 1011 移到了低16位,前面补 0 即可。

0000 0000 0000 1011 1101 0010 1110 1001
0000 0000 0000 0000 0000 0000 0000 1011

两段值进行异或操作,相同得 0 不同得1,结果为 0000 0000 0000 1011 1101 0010 1110 0010 其实不难发现,我们将774889 的高16位与低16位进行了异或运算。

寻址算法的优化,(n-1)& hash ,然后将异或操作后得到的值与数组长度减一进行与运算,可以得到一个数组的一个位置,假设数组长度为16(数组的长度一般为2的幂次方)
hash2:0000 0000 0000 1011 1101 0010 1110 0010
n-1 : 0000 0000 0000 0000 0000 0000 0000 1111 ->15
&结果:0000 0000 0000 0000 0000 0000 0000 0010

4. HashMap 是如何进行扩容的呢?

数组容量有限,当达到一定程度就会进行扩容,java7 是头插法,java8以后是尾插法。数组扩容是以2倍扩容的。
尾插入,就是在链表尾部插入,头插入就是在链表首部插入。其实不难发现,尾插法可以保持原有链表的顺序不变。
当链表进行扩容时,如果是头插法,可能导致链表顺序倒置,原链表的引用关系发生了改变。

上面我们得知,hash值与数组长度减一进行与运算,数组长度扩容时,与运算的结果可能发生改变。
在这里插入图片描述
Capacity:HashMap当前长度。
LoadFactor:负载因子,默认值0.75f

负载因子是0.75f,也就是说当数组空间占到75%的时候就会发生自动扩容。例如当前数组空间为100,当存第76个值的时候,就会自动扩容为原来的空间的两倍200。

4. 1 负载因子为什么是0.75呢?

如果负载因子小一些比如是0.4,那么初始长度16*0.4=6,数组占满6个空间就进行扩容,很多空间可能元素很少甚至没有元素,会造成大量的空间被浪费
如果负载因子大一些比如是0.9,这样会导致扩容之前查找元素的效率非常低
loadfactory设置为0.75是经过多重计算检验得到的可靠值,可以最大程度的减少rehash的次数,避免过多的性能消耗,0.75是对空间和时间效率的一种平衡选择。

5. HashMap 为什么线程不安全?

jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。
jdk1.8中,在多线程环境下,会发生数据覆盖的情况。

  1. 多线程下扩容死循环。JDK1.7中的HashMap使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题
  2. 多线程的put可能导致元素的丢失。多线程同时执行put操作,如果计算出来的索引位置是相同的,那会造成前一个key被后一个key覆盖,从而导致元素的丢失。此问题在JDK1.7和JDK1.8中都存在
  3. put和get并发时,可能导致get为null。线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题,此问题在JDK1.7和JDK1.8中都存在

6. HashMap 的put 方法流程(以jdk8为例)

  1. 首先根据key的值计算hash值,找到该元素在数组中存储的下标
  2. 如果数组是空的,则调用resize进行初始化;
  3. 如果没有哈希冲突直接放在对应的数组下标里 如果冲突了,且key已经存在,就覆盖掉value
  4. 如果冲突后是链表结构,就判断该链表是否大于8,如果大于8并且数组容量小于64,就进行扩容;
  5. 如果链表节点数量大于8并且数组的容量大于64,则将这个结构转换成红黑树;否则,链表插入键值对,若key存在,就覆盖掉value
  6. 如果冲突后,发现该节点是红黑树,就将这个节点挂在树上

7. HashMap 为什么超过8转红黑树,小于6转链表?

从平均时间复杂度来看,红黑树的平均查找长度是logn,如果长度为8,则logn=3,而链表的平均查找长度为n/4,长度为8时,n/2=4,所以阈值8能大大提高搜索速度
当长度为6时红黑树退化为链表是因为logn=log6约等于2.6,而n/2=6/2=3,两者相差不大,而红黑树节点占用更多的内存空间,所以此时转换最为友好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值