HashMap底层数据结构?jdk1.8算法优化,hash冲突,扩容等问题

面试必备系列不会长篇理论求证,直接上答案,仅供参考,不喜勿喷。

1、能说说HashMap的底层原理吗?

HashMap<String,String> map = new HashMap<String,String>(); 

map.put(“key”,”value”); 

[<key1,value1>,<key2,value2>,<key3,value3>] 

HashMap底层实现是数组+链表,用来存储<key,value>形式的数据,当我们调用put(key,value)时,首先会通过hash(key) 来获取key的hash值,hash值对数组长度进行取模运算,定位到数组的一个存储位置 bucket,如果bucket没有发生冲突的话则直接放入数组,发生冲突的话则以链表的形式存储,jdk1.8之后引入了红黑树,链表的长度超过8之后会使用红黑树,小于6之后则又转换回来。

如下2、3条皆为第1条的补充,预防面试官细问。

2、jdk1.8中对hash算法和寻址算法的优化

jdk1.8中对hash算法进行了优化,之前在对key进行hash(key)计算时,采用的是取模运算,即第1条提到的,而优化后采用的是寻址算法,即:(n-1) & hash 『n为数组长度』

为什么要使用寻址算法呢?首先 「hash & (n-1)」 效果跟 hash 对 n 取模的效果是一样的, 但是『&』与运算的性能要优于 hash 对 n 取模。

3、hash冲突?怎么解决?

当我们put<key,value>时,首先通过hash(key)计算得到的hash值,再通过『&』与运算之后,得到了数组存储位置bucket,但此时有可能出现两个不同的key却计算出相等的bucket,举个例子:

数组A[0]位置计算出存放<张三,我是张三>数据,而在put<李四,我是李四>数据时,也计算为存放在A[0]位置,一个位置想存放两个数据?这就出现hash冲突了,怎么处理呢?

JDK是这样处理的,它会在这个位置(A[0])挂一个链表,这个链表表里面存放出现冲突的数据,即:让多个<key,value>数据同时放在数组的一个位置里。

get(key)时怎么取呢?当我们调用get(key)定位到数组位置时,如果发现这个位置挂载的是一个链表,那么就遍历链表,从里面找到自己想要的那个<key,value>数据。

格外补充:这个地方,在JDK1.8之后引入了红黑树的概念,首先我们看一下为啥要引入红黑树,如果没有引入红黑树,当数组挂载的链表达到一定长度之后,查询是非常耗时的,性能比较差,时间复杂度为:O(n)「读作:偶en」。

JDK1.8的优化就是,当链表的长度发到了一定长度后(8)会自动转换为红黑树,遍历一棵红黑树查找一个元素的时间复杂度为:O(logn)「读作:偶,老个en」,性能相对链表要高一些。

简单总结一下:

  1. 出现hash冲突的原因?两个不同的key计算出相同的数组存放位置;

  2. 初期是怎么解决的?在出现数组冲突的位置挂一个链表,实现存放多个数据。

  3. JDK1.8的优化?当数组长度达到一定值后自动转换为红黑树,降低时间复杂度。

4、HashMap是如何扩容的?

HashMap底层是一个数组,当数组满了之后,他会自动进行2倍扩容,用于盛放更多的数据。

比如,本来数组默认长度=16,扩容后*2=32。

扩容后还有一步操作:rehash,重新对每个hash值进行寻址,也就是用每个hash值跟新的数组长度 n-1 进行『&』与运算操作。

补充:扩容之后的与运算可能会导致之前的发生hash冲突的元素不再发生冲突。

延伸一:之前一直在背的面试题中, 提到 HashMap 在多线程是不安全的「死循环」,为啥呢?或为什么说HashMap可能会导致死循环?
 
这个过程就是发生在扩容阶段,在jdk1.7之前,hashmap在扩容到2倍新容器时,由于采用的是头插法「头插法就是总是把新增结点插在头部」,会造成链表翻转形成闭环,也就是形成死循环,jdk1.8之后就不再采用头插法了,而是直接插入链表尾部,因此不会形成环形链表形成死循环,但是在多线程的情况下仍然是不安全的,在put数据时如果出现两个线程同时操作,可能会发生数据覆盖,引发线程不安全,总之,用ConcurrentHashMap没错了。 
 
延伸二:这为什么说HashTable效率低下呢?
 

HashTable使用synchronized关键字来保证线程安全。当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法就会进入阻塞或轮训状态。这个的同步方法包括读和写,可以理解HashTable只有一把锁,所有的线程不管做什么,都是竞争这一把锁,例如线程1使用put进行元素添加,线程2不但不能使用put来添加元素,也不能使用get方法来获取元素,显然这效率是多低。

博客地址:https://www.cnblogs.com/niceyoo

posted @ 2020-03-16 23:45  niceyoo  阅读(2242)  评论(2编辑  收藏  举报