Java中HashSet、HashMap的低层数据结构

以下先讲解HashMap

开始先简单总结:

  • HashMap在JDK8之底层:数组 + 链表
  • HashMap在JDK8之底层:数组 + 链表 + 红黑树

在底层存储的时候必定有数组,链表与红黑树是在特定情况下产生的。

01BCB5C6-6444-4D64-A201-C1FB0BD35822.png

1.HashMap底层

1.1Hash底层存储结构

1.首先HashMap中的Key与Value是存储在Node对象中,作为Node对象的属性出现:


image.png

2.Node对象是存储在Node数组中:


image.png

3.最重要的一点,Node存储在数组中的模式使用的是Set集合的存储思想:无序、不可重复

无序:在存储到Node[]数组的时候,并不是顺序存储,而是根据key的hash值计算得来的存储位置。
不可重复:Key是不可重复的,为什么呢?后面会讲。

现在我们知道了:HashMap集合Key与Value存储在对象中,对象存储在数组中
Key、Value --> 对象 --> 数组
如下图:

image.png

1.2 HashMap为什么是无序的?不可重复的?

1.向HashMap中添加一组值:map.put(key, value);

  1. 计算hash值:这时系统会根据key值,调用key所在类的hashCode()方法计算hash值

所以为什么存储在map中的对象,需要所在类重写hashCode()方法

  1. 计算index值:再根据hash值计算index index = (length - 1) & hash (length当前底层数组的长度) index就是存储在数组中的位置(下标)。

总结:因为Node对象存储在数组中的位置是根据Key值计算得来,所以并不是顺序存储进去的(所以是无序的),同时因为相同的Key计算出来的index也是相同的,当Key相同时会用当前node替换原始的node,但是还是保持一个map中key值是唯一的(不可重复性)体现在下面源码中:

其中 p 是新增的 node,e是原始的 node:如果hash值相同,key相同,就用新增的替换原来的


image.png

我们看到还调用了 key.equals(k), 前面的 k == key 判断地址,后面 equals 判断内容,保持绝对相等

所以我们除了为什么无序、不可重复,还得出结论:存储在HashMap中的对象需要重写 hashCode()equals() 这两个方法

为什么要使用hash值计算index进行无序存储呢?当我们查找一个key对应的value的时候,就不需要顺序遍历底层数组了,直接计算index值,去对应位置拿就行。比list查找效率大大提升。而且map集合设计之初就是用来根据key取value值的存储结构,需要承担大量查找操作。

1.3 链表与红黑树是如何产生的

1.当我们添加一组值的时候,map.put(key, value);通过key的hash值计算得到的index,拿着index去数组中存储的时候发现该位置已经有数据了怎么办?

image.png

2.那么就会先比较 hash 值,再比较 key 地址值,最后比较key的内容是否相等,如果还是相等呢?那么node中的next属性就派上用场了:


image.png

3.系统就会将当前对象node赋值给next,让它链接到原始node的后面(jdk8之后是新node链接到原始的后面,jdk8之前是把新的对象放在数组中,把原始对象链接到新node的后面,总之都是形成一个单向链表)就形成了:数组 + 链表

image.png

4.什么时候会形成红黑树呢?提到下面两个关键的变量:


image.png

5.当链表长度达到8,且数组长度达到64时候就会转换为红黑树进行存储。如果链表长度达到8,数组长度还未小于64,就会先进性扩容。

HashMap的每一次扩容都会重新计算每个node的index,因为 index = (length - 1) & hash,index与数组长度有关。这样也能减少链表与红黑树的出现。

6.所以后续就有可能形成:数组+链表+红黑树 的情况(也不排除偶然:每个计算的index都不相同,只有数组,那就太爽了):

8048D9C0-711B-463A-BA24-AA6DD7B39489.png

1.4 HashMap何时会进行扩容?

List 默认底层默认是长度为10的数组,存满了(进来第11个元素),就扩容。
HashMap 底层默认是长度为16的数组,加载因子是0.75,也就是说存储超过数组长度在75%也就第一次扩容是12个的时候就会进行扩容。


image.png

为什么不存储满了再扩容呢?因为存储位置是计算出来的,万一计算出来的index就是不会出现2,那么2这个位置永远不会存储元素。那就完蛋。
同时,如上面描述的也会在长度未达到64,链表长度就达到8的时候扩容。

现在我们就基本讲完了HashMap的底层了。

2.LinkedHashMap

其实也就是在HashMap的基础上增加了双向指针,指向前一个,后一个,方便遍历。


image.png

3.HashSet

为什么放在最后讲,因为Set集合的底层就是调用了HashMap:
创建的时候构造器new了一个HashMap:


image.png

添加的时候调用HashMap的put方法:


image.png

只不过在一开始new了一个单例的Object作为value:


image.png

4.LinkedHashSet

实际上LinkedHashSet底层就是一个LinkedHashMap

LinkedHashSet构造器调用了HashSet中的构造器


image.png

HashSet中的这个构造器创建LinkedHashMap对象返回。


image.png

总结:
HashMap
默认初始化数组长度:16
扩容倍数:2倍
HashMap的默认加载因子:0.75
Bucket中链表长度大于该默认值,转化为红黑树 : 8
桶中的Node被树化时最小的hash表容量:64

image.png

HashMap 是大哥
LinkedHashMap 就是在 HashMap 基础上增加双向链表
HashSet 底层就是 HashMap
LinkedHashSet 底层就是 LinkedHashMap

你可能感兴趣的:(Java中HashSet、HashMap的低层数据结构)