HashMap原理分析
- JDK7 HashMap
- 1、模型介绍
- 2、底层实现原理
- 3、描述一下put的过程
- 4、HashMap扩容机制:
- 5、HashMap中的循环链表是如何产生的
- 6、HashMap和HashTable的区别
- 7、HashMap为什么用红黑树而不用B树?
- JDK8 HashMap
JDK7 HashMap
1、模型介绍
HashMap在JDK7中采用的基本存储结构都是 数组+链表 形式。它的底层维护一个Entry数组。它会根据计算的hashCode将对应的KV键值对存储到该数组中,一旦发生hashCode冲突,那么就会将该KV键值对放到对应的已有元素的后面, 此时便形成了一个链表式的存储结构。
2、底层实现原理
它基于hash算法,通过put方法和get方法存储和获取对象。
存储对象时,我们将K/V传给put方法时,它调用K的hashCode计算hash从而得到bucket位置。
获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。
如果发生碰撞的时候,HashMap通过链表将产生碰撞冲突的元素组织起来。在JDK 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。
3、描述一下put的过程
1、首次扩容:
先判断数组是否为空,若数组为空则进行第一次扩容(resize)16;
2、计算索引:
通过hash算法,计算键值对在数组中的索引;
3、插入数据:
如果当前位置元素为空,则直接插入数据;
如果当前位置元素非空,且key已存在,则直接覆盖其value;
如果当前位置元素非空,且key不存在,则将数据链到链表末端;
若链表长度达到8,则将链表转换成红黑树,并将数据插入树中;
4、再次扩容
如果数组中元素个数(size)超过threshold(capacity * load factor),则再次进行扩容操作。
4、HashMap扩容机制:
1、数组的初始容量为16,而容量是以2的次方扩充的,一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模运算(据说提升了5~8倍)。
2、数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组。这个0.75就是默认的负载因子,可由构造器传入。我们也可以设置大于1的负载因子,这样数组就不会扩充,牺牲性能,节省内存。
3、为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时(8),且table数组的大小也到达一个阈值时(64),会将链表转换成红黑树以提高性能。而当链表长度缩小到另一个阈值时(6),又会将红黑树转换回单向链表提高性能。
5、HashMap中的循环链表是如何产生的
循环链表的问题只在JDK7之前才会出现,JDK8以后这个问题被彻底解决。
在多线程的情况下,当重新调整HashMap大小的时候,就会存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为在JDK 7中HashMap在进行数据插入时采用的是头插法。如果条件竞争发生了,那么就会产生死循环。
6、HashMap和HashTable的区别
1、HashTable是一个线程安全的Map实现,但HashMap是线程不安全的实现(HashMap在并发执行put操作时,可能会导致形成循环链表,从而引起死循环),所以HashMap比HashTable的性能高一点。
2、HashTable不允许使用null作为key和value,但HashMap可以使用null作为key或value。
7、HashMap为什么用红黑树而不用B树?
B/B+树多用于外存上时,B/B+也被成为一个磁盘友好的数据结构。
HashMap本来是 数组+链表 的形式,链表由于其查找慢的特点,所以需要被查找效率更高的红黑树结构来替换。如果用B/B+树的话,在数据量不是很多的情况下,数据都会“挤在”一个结点里面,这个时候遍历效率就退化成了链表。
JDK8 HashMap
JDK7中HashMap的实现方案有一个明显的缺点,即当Hash冲突严重时,在桶上形成的链表会变得越来越长,这样在查询时的效率就会越来越低,其时间复杂度为O(N)。
JDK8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。它的底层维护一个Node数组。当链表的存储的数据个数大于等于8的时候,不再采用链表存储,而采用了红黑树存储结构。这么做主要是在查询的时间复杂度上进行优化,链表为O(N),而红黑树一直是O(logN),可以大大的提高查找性能。