基础知识
HashMap是基于哈希表的Map接口的实现,是以key-value存储形式存在,即主要用来存储键值对。HashMap的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为Null。另外,HashMap中的映射不是有序的,即HashMap无序。
在详细讲解HashMap(JDK7、JDK8对比)之前我们先了解一些基础知识
在不特别注明的情况下,下文JDK7默认指的是JDK7以及之前的版本,JDK8指的是JDK8以及之后的版本。
概念
什么是哈希表?
在说明哈希表之前,咱们也大概了解一下其他的数据结构的知识,主要是在增删改查方面对比。
数组
采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,时间复杂度为O(n),当然对于有序数组,可以使用二分法查找、插值查找、斐波那契查找等方式,可以将查找的复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,平均复杂度为O(n)。
线性链表
对于链表的新增,删除等操作(找到指定操作位置后),仅需要处理节点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一对比,复杂度为O(n)。
二叉树
对于一颗相对平衡的有序二叉树,增删改查,复杂度都为O(logn)。
哈希表
相比以上结构,哈希表进行增、删、查性能很高,不考虑哈希冲突情况,只需要一次定位即可,时间复杂度为O(1)。
所以我们使用哈希表存储数据有很高的增删改查的效率。
如何实现?
我们要了解数据结构的物理存储结构其实就只有两种:
- 顺序存储结构
- 链式存储结构
栈、队列、树、图都是从逻辑结构去抽象的概念,映射到内存中,其实也都是这2类物理组织形式。
那么哈希表如何利用这2中物理结构呢?
我们回看数组
的结构,只要知道数组的下标就可以快速的找到对应的值,那么哈希表就是利用了这一特性,哈希表的主干就是数组
。
哈希表数据结构和存在的问题
既然我们知道哈希表的主干是数组,那么数据怎么存储的呢?
哈希函数
比如我们要存储一个元素,我们只需要将元素的关键字,通过某个方法映射到数组中的某个位置,然后将它存储起来。那么我们要查询的时候,只需要再次得到这个位置的下标就可以定位到存储的位置,也就可以快速的取出数据。
存储位置 = function(关键字key)
这个方法就是哈希函数
,哈希函数设计的好坏会直接影响到哈希表的优劣。
Hash的音译为“哈希”,直译为散列,是一种信息摘要算法,但是他不是加密。散列算法是一种从任何一种数据中创建小的数字“指纹”的方法。
散列函数把消息或者数据压缩成摘要,使得数据量变小,将数据的格式固定下来。该函数将数据打乱混合,重新创建一个叫做散列值的指纹。
我们平时使用的md5,ssl等都属于hash算法,通过key进行hash计算,就可以获取key对应的hashcode。
![image-20200401093114832](http://img.ybq87.top/upic/2020/0401/image-20200401093114832.png)
哈希表如何存储数据?如上图,假设我们要将key存入下面的一个数组结构中,将key经过hash函数的运算,得到一个存储的内存地址,指向了第2个位置(数组index=2),那么就将key存到这个位置即可。
哈希冲突
万事无完美。
哈希函数毕竟是人为设计的,如果某几个元素经过哈希函数之后得到的值都一样,那么怎么办?当出现这种情况时,我们就说发生了哈希冲突
,或者哈希碰撞
。
大家都听说过哈希冲突,但是没见过实例,这里就列一个:
public class Test {
public static void main(String[] args) {
System.out.println("重地".hashCode());
System.out.println("通话".hashCode());
}
}
// 打印结果
1179395
1179395
可以看到这2个明显不一样的字符串,经过hash方法之后得到的数值是一样的。
所以哈希函数的设计至关重要,好的哈希函数会尽可能的保证计算简单和散列地址分布是均匀的。但是我们要清楚,数组是一块连续的固定长度的内存空间,它不是无限的,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。
那么如何解决这个问题呢?哈希冲突的解决方案有很多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法。
HashMap采用了链地址法,也就是数组+链表
的方式**拉链法
**(即一系列链表为数组元素组成的数组)。
JDK7:hashmap实现是数组+链表
JDK8:hashmap实现是数组+链表+红黑树
数据结构多重要
下面简单介绍下,还是先画图
![image-20200401094322412](http://img.ybq87.top/upic/2020/0401/image-20200401094322412.png)
如上图,我们存了key到数组了,然后继续插入tom,经过计算得到index=3,将tom存入第四个位置。但是这时候又来了一个lucy,经过hash计算得到的地址也是3(发生了hash碰撞),怎么办。那么就用单向链表将lucy挂到tom的下面去。
JDK7:头插法,在链表头部插入数据
JDK8:尾插法,在链表尾部插入数据
好了,一些基础知识普及完毕,下面开始介绍HashMap的设计逻辑和底层源码分析。
HashMap底层数据结构
下面是一个典型的JDK8的hashmap的数据结构图,我们来一步一步解构它。
![image-20200401100322354](http://img.ybq87.top/upic/2020/0401/image-20200401100322354.png)
开始之前我们写段代码,结合代码来说明下hashmap的设计
public class Test {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map.put("路飞", 5);
map.put("索隆", 4);
map.put("乔巴", 1);
map.put("路飞", 10);
System.out.println(map);
}
}
查看输出
{乔巴=1, 索隆=4, 路飞=10}
可以看到,路飞的悬赏由原来的5亿变成了10亿,而且看到map的打印顺序和我们添加的顺序不一样进一步说明hashmap是无序的。
![斗图点赞表情包 厉害了word哥](http://img.ybq87.top/upic/2020/0401/d2d6a4d9b3e01577becfaae1ba4991fe.jpg)
我们来分析一下这个代码的执行顺序一步一步的解构
HashMap<String, Integer> map = new HashMap<>();
这一句,向内存申请了一块大小为16的数组位置,至于为什么是16,稍后解答。
咱们先画图,(啥也不懂,就先画图)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P4fYjvlI-1585752124102)(http://img.ybq87.top/upic/2020/0401/image-20200401103827517.png)]
- 第一句执行后,在内存划了一块16格的内存给他使用(先不着急区分JDK7和JDK8)。
在jdk7的时候,构造方法是创建一个长度为16的Entry<K,V>[] table
数组,用来存储键值对。
jdk8的时候,改成了使用Node<K,V>[] table
数组来存储键值对,但是数组的初始化是放到了第一次put数据
的时候。至于Entry其实就是名称改成了Node,变量名table都没变。
map.put("路飞",5);
- 执行这一句的时候,hashmap使用**
某个算法
**,将路飞
这个关键字作为参数传入,然后经过计算得到一个数组的index
,假设index=13,那么从数组找到这个13位置,看看这个位置是否有数据,没有数据,就将键值对存进去。
这样看这个某个算法就很重要了,先来窥探一下,当然可以在后面看源码更清晰。
将
路飞
字符串使用String.hasCode()方法得到他的hash值,假设"路飞".hashCode()=1314
,然后这个hash
会结合数组table的长度length
,进行一系列的无符号右移(>>>)、按位异或(^)、按位与(&)操作,计算出数组的索引index
。简单来看这个方法就是:
index = hash & (length - 1)
那么问题来了!
hashmap计算索引index使用了那么多算法,为什么不直接使用取余的方法?
17%16 --> 1 ;比如17 对数组长度16取余,就是1
22%16 --> 4 ;22对数组长度16取余,就是4,
虽然这2个数计算出来的index不一样,但是当我们扩大插入的数据时,比如插入33
33%16 --> 1 ;出现的hash碰撞,
那么我们再试试另外一种算法,
看看源码简单的(代码是JDK8的,不过JDK7的也一样),下面的方法,在put方法中,调用了putVal,然后我们关注到这个hash(key)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
查看一下源码:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这一句简化一下:key = “路飞”,“路飞”.hashCode() = 1314
int h = key.hashCode();// h = 1314
return (h)^(h >>> 16); // 1314 ^ (1314 >>> 16)
...................我们看看这个计算的结果
1314 = 1024 + 256 + 32 + 2;换算成 二进制就是
0101 0010 0010;
1314 >>> 16 = 0
那么就是
0101 0010 0010
^0000 0000 0000
------------------(异或^的定义是:相同则结果为0,不同则结果为1)
1010 1101 1101 = 2781
从而得到了hash = 2781值。按照我给出的简化公式:index = hash & (length - 1)
index = 2781 & 15
(&的定义:都为1才为1,否则为0)
------------------
1010 1101 1101 = 2781
&0000 0000 1111 = 15
-------------------
0000 0000 1101 = 13
最后计算出index = 13,将路飞放入角标为13的数组内。
好了下面解答为什么用异或,与这类的不用%,位运算效率高于%,而且33%16其实结果和33&(16 - 1)相等。
33 % 16 = 1
33 & (16 - 1) = 33 & 15 算一下:
0010 0001
&0000 1111
------------
0000 0001 = 1
所以当然选择使用位移等操作,效率高嘛。
![老哥,稳。](http://img.ybq87.top/upic/2020/0401/1540126551039.jpg)
好了我们继续,
- 好不容易执行完了一个put,下面简化一下后面的
map.put("索隆", 4);
map.put("乔巴", 1);
- 然后路飞又来了,他的赏金增加了。经过前面的计算我们知道路飞这个关键字经过hash之后,得到的index = 13,然后就会去13这个位置找内存。看看这里有没有数据存储,一看有数据了。下面就开始进一步的比较,已经存储的数据的key的hashcode是否和新传入的一致?再一致的话,就用equals对比下。
可能大家会怀疑为啥还要对比hashcode啊,还记得我们文章开头给出的例子嘛?
对比的结果有2种:
- 相等:那么就将新的value覆盖已有的value,路飞的悬赏变为了10。
- 不相等:要是2个equals方法比较出来不相等(没错,我们可以重写equals方法,让路飞不equals路飞),那么就向下找(注意这里是个单向链表),如果都不相等,就在链表尾部加上这个新的key-value。
![image-20200401133857047](http://img.ybq87.top/upic/2020/0401/image-20200401133857047.png)
好了,我们已经摸到了门槛了。
上面的解析只是JDK7的设计逻辑,那么JDK8呢?其实JDK8只是增加一个红黑树。当单向链表里面的数据已经达到了8个,如果又来了一个新的数据要加入到这个链表,那么JDK8会将这个链表转化为红黑树。反之,如果从红黑树删除了元素,当elements.length <= 8的时候,红黑树会转化为单向链表。
好了这块拓展就不深入,这里不讲红黑树,你只要知道红黑树也是一种存储数据的结构,它的查询效率是O(logn)。
面试官:为什么要使用红黑树?单向链表不香嘛?而且为什么是8个之后转为红黑树?
因为在数据量小的时候,单向链表的查询效率很高,随着数据量增加,单向链表的查询时间复杂度是O(n),而红黑树的时间复杂度是O(logn),所以综合考虑选定了8这个节点,将链表转为红黑树。
源码解析(默认JDK8)
我们来看看几个需要认识的关键成员变量
/**
* 默认初始容量16(必须是2的幂次方),至于为什么是2的幂次,后面讲
* table的默认初始值,如果在new HashMap()没有指定参数的时候,使用这个值创建数组
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* 最大容量,2的30次方,但是注意,并不是说hashmap的table最大只能存储这么多数据,这个参数只是说用户在初始化hashmap的时候,传参最大只允许到 1<<30;
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认加载因子,用来计算threshold,为啥是0.75呢?不是1,不是0.5?
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 链表转成树的阈值,当桶中链表长度大于8时转成树
threshold = capacity * loadFactor
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 进行resize操作时,若桶中数量少于6则从树转成链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 桶中结构转化为红黑树对应的table的最小大小
当需要将解决 hash 冲突的链表转变为红黑树时,
需要判断下此时数组容量,
若是由于数组容量太小(小于 MIN_TREEIFY_CAPACITY )
导致的 hash 冲突太多,则不进行链表转变为红黑树操作,
转为利用 resize() 函数对 hashMap 扩容
* ---------
* 用普通人能够理解的话说就是:当数组长度大于64的时候,才将链表转化为红黑树,而不是简单的链表长度大于8了就开始转。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
保存Node<K,V>节点的数组
该表在首次使用时初始化,并根据需要调整大小。 分配时,
长度始终是2的幂。
*/
transient Node<K,V>[] table;
/**
* 存放具体元素的集
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* 记录 hashMap 当前存储的元素的数量,因为并不是每个数组的位置上都会有值,肯定有空位,所以记录下有值的个数
*/
transient int size;
/**
* 每次更改map结构的计数器
*/
transient int modCount;
/**
* 临界值 当实际大小size > (容量table.length * 填充因子 DEFAULT_LOAD_FACTOR)超过临界值时,会进行扩容
*/
int threshold;
/**
* 负载因子:要调整大小的下一个大小值(容量*加载因子)。默认0.75,可以自定义
*/
final float loadFactor;
上面的注释初步将用到的变量进行的说明,但是各个变量怎么用,为什么这么用呢?
我们一步一步的看源码。
![image-20200401135831977](http://img.ybq87.top/upic/2020/0401/image-20200401135831977.png)
如何初始化HashMap
诶,有老铁可能要问了,初始化hashmap这还是个问题嘛?难道不是直接new?这个问题看似简单,其实里面有很多学问,而且我们如果使用IDEA,并且安装了阿里巴巴的代码检查插件,在我们new hashmap的时候,他会提示一句:
![image-20200401140149564](http://img.ybq87.top/upic/2020/0401/image-20200401140149564.png)
下面有个烦人的波浪线,有强迫症的老铁要受不了了。我们就来看看这个构造函数到底什么意思
我们点进源码,从structure看到有4个构造函数
![image-20200401140402304](http://img.ybq87.top/upic/2020/0401/image-20200401140402304.png)
这里我们只研究其中的2个HshMap()
和HashMap(int)
/**
* Constructs an empty {@code HashMap} with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
我们看到默认的构造函数,初始化的时候,将加载因子赋值为0.75。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 调用第三个构造函数,加载因子默认 0.75,
/**
* 传入初始容量大小和负载因子 来初始化HashMap对象
*/
public HashMap(int initialCapacity, float loadFactor) {
// 初始容量不能小于0,否则报错
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 初始容量不能大于最大值,否则为最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//负载因子不能小于或等于0,不能为非数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 初始化负载因子
this.loadFactor = loadFactor;
// 初始化threshold大小
this.threshold = tableSizeFor(initialCapacity);
}
这里我们看到可以接受一个initialCapacity
参数,假设我们这么写:
HashMap<String,Integer> map = new HashMap<>(10)
initialCapacity = 10,进入了tableSizeFor(initialCapacity)
方法
/**
* 找到大于或等于 cap 的最小2的整数次幂的数。
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
按照这个算法,10的最接近的2的幂次是16,所以this.threshold = 16
那么这个threshold是什么玩意?后面就会见到了,这里按下不表,我们只需要知道你传入的参数最后不一定就真的是用到的实际数值。
DEFAULT_INITIAL_CAPACITY 为何必须是2的幂次方
看一段代码,我们假设下面的定义是创建一个长度为7的table数组
HashMap<String,Integer> map = new HashMap<>(7)
map.put("路飞",5)
在之前我们讲解hashmap的数据结构的时候,我们知道在hashmap中添加一个数据,需要计算key的hash值,然后按照某种规则得到他在数组中的index。
之前我们也得到了这个公式index = hash & (length - 1)
现在 length = 7,length = 16,我们,取几个数分别对比一下结果
@Test
public void testL() {
Set<Integer> nums = new HashSet<>();
for (int i = 0; i < 20; i++) {
int hash = org.apache.commons.lang3.RandomUtils.nextInt(1000, 5000);
nums.add(hash);
}
for (Integer hash : nums) {
System.out.println((hash & (7 - 1)) + "<====>" + (hash & (16 - 1)));
}
}
打印:
2<====>3
2<====>3
6<====>6
6<====>6
0<====>9
2<====>10
2<====>11
2<====>11
4<====>12
4<====>13
4<====>13
4<====>13
6<====>15
0<====>1
2<====>2
4<====>4
0<====>8
0<====>9
2<====>10
2<====>11
可以观察到,基本上右侧的数据都是很均匀的占用了1~15这些位置,但是左边的只有 0、2、4、6这些,利用效率非常低。
我们就得出一个结论,之所以设置为2的幂次,其实是为了让hash值均匀的分布在数组上,从而充分的利用空间,而一些非2幂次的数虽然也可以用,但是使用效率不高。
为何加载因子是0.75
加载因子是什么,有什么作用?我们首先要知道一个概念,hashmap中的table数组因为是有初始容量的,当我们要存储的数据大于这个容量怎么办呢?就需要扩容了。
但是扩容不是盲目的扩容,应该选择一个合理的计算手段,不然就浪费内存,内存资源还是比较紧张的。
而这个扩容因子就和扩容有关。
假设有语句
HashMap<String,Integer> map = new HashMap<>()
成员变量就都是默认的,那么加载因子为0.75,默认长度为16,当table中的数据size > (16 * 0.75)的时候,table就要进行扩容。
此时的各类参数的值为:
threshold = 16
loadFactor = DEFAULT_LOAD_FACTOR = 0.75
DEFAULT_INITIAL_CAPACITY = 16
那么为什么要定义这个加载因子是0.75呢?不是1,不是0.5,0.7?
其实还是从效率上来考虑,假设设置为1,那么就是16个位置全部占满了才扩容。我们知道hash计算是会出现碰撞的,那么就会产生链表结构,链表再快也没有数组中直接通过角标获取的快吧。所以为了尽量使数据落在数组上,要减少链表,那么就不要用1了。我们用0.7可以吗?
16 * 0.7 = 11.2,这样计算出来还是个float,我们划空间不可能给小数,你说取整就好了。那为什么不一开始就定为0.75,这样计算出来的全部是整数。因为 数组的初始长度都是2的幂次,2^n * 0.75结果都是整数,不需要其他计算了。
那么有人说设置的小一点如何,0.5?
那就是16 * 0.5 = 8,占用了8个位置就要划新的区域给他,这个hashmap也太吃内存了吧。要知道扩容的时候是之前的空间的倍增方式扩容的。
newThr = oldThr << 1; // double threshold
16 << 1 = 32;万一你只需要存储9个数据,结果使用0.5因子,划了32个空间给他,多浪费。
所以这个0.75其实是一个综合考虑性能和内存空间的结果。
除非你非常确定你的代码中数据占用的位置,否则还是使用默认的构造函数吧
。
put方法
下面分析一下put方法都是什么原理
public V put(K key, V value) {
// 调用hash(key)方法来计算hash ,这里我们上面看到了:hashcode ^ (hashcode >>> 16)
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, // 记录数组长度
i;
// 容量初始化:当table为空,则调用resize()方法来初始化容器table
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//确定元素存放在哪个桶中,桶为空,新生成结点放入桶中,这里就是计算hash值得到数组的index,存数据
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//如果键的值以及节点 hash 等于链表中的第一个键值对节点时,则将 e 指向该键值对
e = p;
// 如果桶中的引用类型为 TreeNode,则调用红黑树的插入方法
else if (p instanceof TreeNode)
// 放入树中
e = ((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) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//判断要插入的键值对是否存在 HashMap 中
if (e != null) { // existing mapping for key
V oldValue = e.value;
// onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 键值对数量超过阈值时,则进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
resize方法和扩容机制
table的默认长度是2的幂次,而默认阈值threshold = 16 * 0.75 = 12,当table中数据个数大于12的时候,就要进行扩容。
数组扩容按照当前table的长度的2倍进行,比如当前16,就扩容到32,相应的threshold = 32 * 0.75 = 24了。
扩容后,重新计算键值对的位置,然后移动他们到合适的位置。
final Node<K,V>[] resize() {
// 拿到数组桶,刚new hashmap,啥都没有 table = null,threshold = null
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;// 数组长度
int oldThr = threshold;// 还记得我们上面介绍的嘛?threshold ,不过因为我们没有在构造函数传值,所以这里threshold = null
int newCap, newThr = 0;
// 如果数组桶的容量大于0,显然第一次put的时候 oldCap = 0,我们看else if 方法去
if (oldCap > 0) {
// 如果比最大值还大,则赋值为最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 如果扩容后小于最大值 而且 旧数组桶大于初始容量16, 阈值左移1(扩大2倍)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 左移1位
newThr = oldThr << 1; // double threshold
}
// 如果数组桶容量<=0 且 旧阈值 >0
else if (oldThr > 0) // 这里的 oldThr = threshold = 16
// 新容量=旧阈值
newCap = oldThr;
// 如果数组桶容量<=0 且 旧阈值 <=0
else { // zero initial threshold signifies using defaults
// 新容量=默认容量
newCap = DEFAULT_INITIAL_CAPACITY;
// 新阈值= 负载因子 * 默认容量 = 0.75 * 16 = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新阈值为0
if (newThr == 0) {
// 重新计算阈值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 更新阈值
threshold = newThr; // 初始化的时候 threshold = 16了。
@SuppressWarnings({"rawtypes","unchecked"})
// 创建新数组,啊哈,看到了吧,创建数组的时候用到了 newCap = oldThr = threshold = 16
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 覆盖数组桶
table = newTab;
// 如果旧数组桶不是空,则遍历桶数组,并将键值对映射到新的桶数组中
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果是红黑树
else if (e instanceof TreeNode)
// 重新映射时,需要对红黑树进行拆分
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 如果不是红黑树,则按链表处理
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 遍历链表,并将链表节点按原顺序进行分组
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 将分组后的链表映射到新桶中
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
treeifybin方法转为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 先去判定当前的table长度是否大于64,小于64位的数组,优先进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 如果节点不为null,将node节点转为 treenode 加入到树中,
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
get方法
查找方法就相对简单很多了,
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// 获取hash值
static final int hash(Object key) {
int h;
// 拿到key的hash值后与其五符号右移16位取与
// 通过这种方式,让高位数据与低位数据进行异或,以此加大低位信息的随机性,变相的让高位数据参与到计算中。
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab;
Node<K,V> first, e;
int n; K k;
// 定位键值对所在桶的位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 判断桶中第一项(数组元素)相等
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 桶中不止一个结点
if ((e = first.next) != null) {
// 是否是红黑树,是的话调用getTreeNode方法
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 不是红黑树的话,在链表中遍历查找
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
![image-20200401162307913](http://img.ybq87.top/upic/2020/0401/image-20200401162307913.png)
老铁们关注一波