一、HashMap
1、几个重要变量
size : 当前元素个数
modCount:这个变量在每次执行更新的时候都会加一
loadFactor:加载因子,用于计算阈值,一般在0~1之间threshold:这个变量表示容量阈值,超过这个阈值就需要扩容,threshold = capacity * loadFactor
DEFAULT_INITIAL_CAPACITY :初始化大小,为16
DEFAULT_LOAD_FACTOR:默认的加载因子,为0.75
MAXIMUM_CAPACITY:最大允许的容量,为107374182
2、底层数据结构
2.1、Node节点
如图所示
每个节点包含key的hash值、key、value以及指向下一个节点的指针。key的hash值和key在初始化node后就不能变,value可以改变。不同的key由不同的hash值,但是映射到容量有限的数组中时,可能映射到同一个位置,这时候就发送了hash冲突,这就像取模运算,3 mod 2 和 5 mod 2的结果是一样的,但是3不等于5。对于冲突的数据,通过头部插入的方式形成一张链表,从而解决冲突。
2.2、Node数组
HashMap底层使用一个Node数组保存数据,对于数组同一个位置的不同key,则通过链表的形式保存,因此HashMap底层数据结构就是数组加链表:
2.3、扩容
HashMap默认初始大小为16,加载因子为0.75,因此阈值为12,当向HashMap插入第13个元素时,会发生扩容。HashMap每次扩容都是原来容量的两倍,阈值也会重新调整。HashMap会申请一个新的数组,然后把数据全部重新散列到新的数组,由于每次扩容2倍,因此原来位置为2的倍数的节点,重新散列后节点位置不会变。其他元素(包括hash冲突导致的链表上的元素)则会重新散列。
3、put操作
put操作顺序如下:
1、计算key的hash值
2、判断当前容量是否达到阈值,如果达到则进行扩容
3、通过key的hash值计算数组位置,如果不冲突,则直接new一个Node节点,然后放在数组位置上
4、如果冲突,判断已存在的Node节点的key值以及hash值和put的是否一样,如果一样,就用新值替换旧值,如果不一样,则new一个Node节点插入到头部,并将Node节点的next指向已存在的节点。
5、size加一,然后再次判断是否要扩容
4、get操作
get操作执行顺序如下:
1、计算key的hash值
2、根据key的hash值计算数组位置
3、如果数组位置有Node节点,则遍历这个Node节点的链表,当且仅当key以及hash都一样的时候,则认为找到了,然后返回value值
4、如果没找到则返回null
5、remove操作
1、计算key的hash值
2、根据key的hash值计算数组位置
3、如果数组位置有Node节点,则遍历这个Node节点的链表,当且仅当key以及hash都一样的时候,则认为找到了,然后去掉这个节点
4、size减一
6、containsValue操作
先循环外层Node数组,然后循环遍历Node链表,然后比较key和key的hash值,如果一样则表示存在,直接返回true,否则返回false。
7、JDK8的改进
上面几节的描述都是基于JDK7,在JDK8之前的HashMap版本,由于底层数据结构是数组加链表的结构,当hash冲突很严重的时候,这个HashMap会退化成一个链表,从而get操作以及put操作从O(1)退化成了O(n)。如下图:
JDK8对这部分进行了改造,Node链表如果节点小于8的时候,依然是链表结构,如果节点数量大于8,则会把链表转换成红黑树,由于红黑树的操作为O(logn),因此即使在冲突很严重的情况下,也可以保证较好的性能。如图:
红黑树本质是一颗平衡二叉树,因此它会通过hash值进行划分,每个节点的左子树的hash值一定比该节点的hash值小,每个节点的右子树hash值一定比该节点的hash值大。此外,红黑树会保持下面5条性质:
1、每个结点要么是红的要么是黑的。
2、根结点是黑的。
3、每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。
4、如果一个结点是红的,那么它的两个儿子都是黑的。
5、对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。从而保证它的插入和获取的算法复杂度为O(logn)。
为了实现上面的5个性质,红黑树在插入数据的时候会动态调整整颗数的结构,通过旋转、分裂等操作实现,具体算法可以参考TreeMap或者数据结构教程。
二、HashSetHashSet
底层基于HashMap,它的add方法传入一个值,这个值作为key插入HashMap,对应的value使用一个共享的Object对象。这样当传入的值相同的时候,就不会新生成Node,而是用共享的Object对象进行一次覆盖,本质上没有任何影响。这样就实现了Set中无重复数据的目的。