秋招复习笔记系列目录(不断更新中):
- 1.数据结构全系列
- 2.计算机网络知识整理(一)
- 3.计算机网络知识整理(二)
- 4. Java虚拟机知识整理
- 5.计算机操作系统
- 6.深入理解HashMap
- 7.深入理解ConcurrentHashMap
- 8.MySQL
一、介绍
1.1 前言
最近在研究Java集合的内容,本来想像前面那样将整个集合都放在一篇博客里面,但发现HashMap、ConcurrentHashMap还有LinkedConcurrentHashMap里面的知识太多太多了,而且拜读了源码以后,才真正地感受到了集合设计者的厉害,所以还是把这三个集合单独列出来写博客来理解吧。在看的过程中,看了很多博客,感觉有些东西其他博客还是没有讲清楚,这里我将我遇到的每个问题都详细的查了,在这里做一个记录,希望能帮助到有需要的同学。
1.2 基础知识
1. 红黑树
红黑树的性质
- 每个节点或者是红色的,或者是黑色的;
- 根节点是黑色的;
- 每个叶子结点(红黑树中叶子节点为最后的空节点)是黑色的;
- 如果一个节点是红色的,那么他的孩子都是黑色的
- 从任意一个节点到叶子节点经过的黑色节点是一样的。
红黑树的插入过程:
红黑树的插入遵循以下规则:
-
1.如果根节点为空,直接插入,并设置根节点为黑色
-
2.根节点不为空,将新插入的节点cur标记为红色。如果cur的父节点不为黑色,在则需要分情况讨论
-
①cur为红,parent为红,pParent为黑,uncle存在且为红,则将parent,uncle改为黑,pParent改为红,然后把pParent当成cur,继续向上调整。
-
②左左: cur为红且为parent的左孩子,parent为红,pParent为黑,uncle不存在或uncle为黑,且parent为为pParent的左孩子,则进行右旋
-
③右右: cur为红且为parent的右孩子,parent为红,pParent为黑,uncle不存在或uncle为黑,且parent为pParent的右孩子,则进行左旋
-
④左右: cur为parent的右孩子,parent为pParent的左孩子,则先左旋转换成左左情况,再右旋
-
⑤右左: cur为parent的左孩子,parent为pParent的右孩子,则先右旋转换成右右情况,再左旋
-
2. 散列的相关知识
1.常见的几种哈希函数
- ①直接寻址法: 以关键字的某个线性函数值为哈希地址,可以表示为 hash(K)=aK+C。优点是不会冲突,缺点是空间复杂度很高
- ②数字分析法: 该方法是取数据元素关键字中某些取值较均匀的数字来作为哈希地址的方法,这样可以尽量避免冲突。缺点是只适合于能预估出全体关键字的每一位数字出现的频率
- ③除留取余法: 最为常用。是由数据元素关键字除以某个常数所留的余数为哈希地址,可以表示为:hash(K)=K mod C。C通常取哈希表的长度。
- ④平法取中法: 对关键字计算平方,然后根据可使用空间的大小取中间分布较均匀的几位,散列到相应的位置。
这样计算的原因是因为关键字的大多数位或所有位对结果都有贡献,并且通过取平方扩大差别,平方值的中间几位(位数可用lgN计算)和这个数的每一位都相关,则对不同的关键字得到的哈希函数值不易产生冲突,由此产生的哈希地址也较为均匀。
2.冲突的解决
-
①开发地址法(再散列法): 主要有以下几种再散列的方式:
- 线性探测法:从产生冲突的位置开始,一个一个往下探测,看是否存有数据,没有则存入
- 二次探测法:从产生冲突的位置开始,在左右两边进行跳跃式探测
- 伪随机探测法:建立一个伪随机数生成器,生成探测位置
-
②链地址法: HashMap的实现方式,用数组当桶,发生冲突时,在桶中以链表链接
二、HashMap
2.1 HashMap的结构
在JDK1.8中,HashMap使用链地址法的方式来实现,其先用一个数组作为桶,然后桶中存放的要么是链表(链表的节点个数小于等于8),要么是红黑树,要么为空。整个数据结构如下所示:
2.2 扩容方法
HashMap每次扩容,容量都是2的幂次方。下面的方法是获得大于cap且最近的2的整数次幂的数。如输入10,则返回16。其中cap-1
的的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。显然不是结果。减1后二进制为111,再进行操作则会得到原来的数值1000,即8。
举个栗子,如果传入参数10,那计算的过程如下:
2.3 对象hash值和在桶中位置的计算方法
举个栗子:
这样做的好处是,可以将hashcode高位和低位的值进行混合做异或运算,而且混合后,低位的信息中加入了高位的信息,这样高位的信息被变相的保留了下来。掺杂的元素多了,那么生成的hash值的随机性会增大。
知道hash值后,根据hash值来计算其在数组中的位置,计算公式为:
pos = (n - 1) & hash
(非常重要),其中n为数组的长度。由于null对象不能计算hash值,所以null对象都放在桶中的固定位置,HashMap中使用第0个桶来存放键为null的键值对。
2.4 查找元素的过程
先根据key
的hashCode
计算得到hash值,然后利用pos = (n - 1) & hash
计算其在数组中的位置,并在该位置中看,该位置存的是链表还是红黑树,然后再在链表/红黑树中进行查找。
2.5 添加元素的过程(put操作)
主要通过putVal()
函数实现,putVal
的过程中,如果没有达到链表的阈值长度(默认是8),则直接加入到链表的尾部。如果链表长度超过8,则将链表变化为红黑树。代码如下,必要的地方都做了详细的注释:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
HashMap.Node<K,V> e; K k;
if (p.hash == hash && //这种情况下,说明放入了重复元素,需要根据key更新value
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof HashMap.TreeNode)//这种情况下,在红黑树中插入节点
e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//这种情况下,在链表中插入节点
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//将新节点插入到链表的末尾
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) //这里链表长度大于阈值,需要将链表转为红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&//链表中已经存在key了,需要根据key更新value
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) {
// 链表中存在key的值的更新
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//增加修改次数
if (++size > threshold)
resize();//如果容量大于阈值了,就去扩容
afterNodeInsertion(evict);
return null;
}
2.6 扩容的过程
通过resize函数将数组扩容为原来容量的2倍后,需要重新将原有的元素映射到数组中,映射时,分以下三种情况:
-
如果原来的位置只有一个节点,直接通过上面的
pos = (n - 1) & hash
计算其在数组中 的位置。 -
如果是链表,进行链表的rehash时,根据
hash & oldCap
的结果是0还是1,将链表拆分成两部分。
举个栗子(栗子来自于别人的博客,我找不到出处了):如果原数组的容量为16,那n-1=15
,然后有两个Entry,第一个Entry的key的hashCode值为1111 1111 1111 1111 0000 1111 0000 0101
,第二个Entry的key的hash值为1111 1111 1111 1111 0000 1111 0001 0101
,那在扩容之前,通过pos = (n - 1) &