HashMap
版本:1.8
随便new
一个HashMap
然后进入其中.
文中不复制过多的源码,可以比较着看.
1.结构分析
1.1 变量:
所有的变量都在这里了
/**
* defaultInitialCapacity
* 默认初始容量(16),
* 用来新建map容器时作为参考.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* maximumCapacity
* 最大容量(1<<30)
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* defaultLoadFactory
* 默认加载因子(0.75)
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* treeifyThreshold
* 树起点.(8)
* 由链表转化为红黑树存储的阈值.
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* unireeifyThreshold
* 不是树的起点.(6)
* 转化为链表存除的阈值.
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* minTreeifyCapacity
* 最小树容量(64)
* 当桶中的结构转为红黑树的最小table大小.
* 当table<64 && hash冲突太多时,改用扩容操作,而不是转化为红黑树存除.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 下面四个变量都用的transient修饰,表示的是不需要进行序列化.
* 都是临时存在的.
*/
//保存Node节点的数组,在首次使用时初始化,并根据需求调整大小.
//我们文中提到的桶指的就是该数组.
//这个桶表示的是链表||红黑树起始节点的数组,比如三个<K,V>,可能只有两个在table中.其中一个因为冲突,在以table位起点的链表中.
transient Node<K,V>[] table;
//存放元素的集合
transient Set<Map.Entry<K,V>> entrySet;
//记录hashMap的大小
transient int size;
//mod的计数器,用来修改map结构.
transient int modCount;
//临界值 当实际大小(容量*加载因子)超过临界值时,进行扩容
int threshold;
//加载因子.在扩容的时候需要,计算threshold=(CAPACITY*loadFactory)
final float loadFactor;
1.2 内部类
可以看出来大概分了三类:
- 迭代器的一类:
iterator
和Spliterator
各4个. - 赋值的一类:
key,value,entry
有3个. - 内部结构的一类:
Node
和TreeNode
分别表示链表结构和红黑树结构.
迭代器和赋值先忽略,我们重点分析一下结构.
1.2.1关于Node<K,V>
自己查看源码可发现.实现了Map.Entry<K,V>
接口.是一个单项链表(只有一个next).
1.2.2 关于TreeNode<K,V>
.
继承了LinkedHashMap.Entry<K,V>
类.实现方法有很多.通过其中的变量可以看出来是一个红黑树map.
然后我们通过从构造,到增删改查,再到调整结构来分析一下各个参数的作用.
1.3 工具方法
在HashMap
中有一些基本的方法.
hash(Object key)
:我们重新得到了一个Object的哈希值:通过高16位和低16位异或运算得出的哈希值.- 求位置运算:我们在源码中常见的一个运算是
((n-1)&hash)
.这个运算的目的是为了通过对桶的大小取模,找出该元素所对应的哈希桶的位置.然后再进一步的寻找.
2. 流程分析.
2.1 构造方法
HashMap
中重载了4个构造方法.可以看出来,都是设置loadFactory
和threshold
属性,这个属性就是我们上面提到的加载因子
和初始容量大小
.
然后转化为合法的大小.比如容量只能是2的整次幂.通过一些运算将容量合法.
PS:这个获取合法初始容量大小的方法tableSizeFor
也小优化了一下.通过1,2,4,8,16
的五次位运算.就可以求出最小的值.确实厉害.(有兴趣的自行了解一下,并不难)
2.2 查找.
查找就是普通的查找方法,没什么难度的.
可以看出来,当我们调用get(Object key)
方法后,
- 首先通过
hash(Object key)
获得了这个key
的哈希值, - 然后通过哈希值和这个
key
调用了真正的查找函数getNode(int hash,Object key)
. - 这个查找函数经过查找后返回一个
<K,V>
对象.然后我们再返回value
.
进入getNode()
函数
tab
是用来确定长度,first
确定位置,e
是目标键值对.- 首先找到键值对所在桶的位置.
((n-1)&hash)
是对hash取模,因为位运算要快于模运算. - 判断桶中第一个元素是否是我们要找的.如果是的话就返回
first
. - 桶中不止一个节点的话,通过
instanceof
关键字判断一下是否是红黑树,如果是的话,通过getTreeNode
来找,否则直接遍历这个链表然后查找.
在TreeNode
中的getTreeNode
.
- 如果当前节点是根节点,就直接在当前所在的红黑树中调用
find
,否则找到根节点调用find
. - 在红黑树中也是,左右子节点伸下去.,找到目标节点就返回
TreeNode
对象.
总之,查找就是根据hash和key来判断
2.3 添加
首先找到put(K key,V value)
函数.看到了它调用了putVal
函数,并且传入了hash值.
在putVal
中分析:
- 先定义了变量,
tab
表示桶,p
表示要添加的元素.n
表示的数组长度. - 在第一个if中我们知道,先判断了桶中元素是否为空,如果为空的话需要进行扩容操作
resize()
,然后重新赋值tab
和n
. - 通过
求位置运算(上文中介绍了)
可以得到该元素在桶中的位置,如果没有发生hash碰撞
,就直接将这个点存进去,否则进行else
步骤. else
中:hash值相同,但是key不同的时候,表示的是该元素在链表||红黑树中.- 我们定义了两个变量:
e
表示的是要修改的目标节点,k
表示的是key. - 首先进行了一个判断起使位置是否是目标节点.如果是目标节点直接得到
e
. - 然后判断是红黑树节点还是链表.红黑树调用
putTreeVal
方法. - 如果是链表遍历链表,
- 如果不存在key,就在末尾添加一个.然后判断一下是否需要树化
treeifBin()
. - 如果遍历中存在key,直接获取
e
.
- 如果不存在key,就在末尾添加一个.然后判断一下是否需要树化
- 最后如果
e
不为空,表示的就是原HashMap
中存在一个相同key的节点e
,我们将其修改一下即可.
- 我们定义了两个变量:
- 所有操作都做完后,判断一下是否需要扩容
(size>threshold)
.
可以看出来,putVal()
函数主要干了4件事:
- 将
table
合理. - 向对应的位置放元素,如果是链表则放入链表,如果是红黑树则放入红黑树.
- 链表的如果需要转化为红黑树的转化为红黑树.
- 再进行扩容.
流程用一个网图来说明:侵删
看一下扩容函数resize()
.
- 定义了几个变量.
oldTab
表示旧的table表
(当第一次添加元素的时候位null
).oldCap
表示旧的容量,oldThr
表示旧的扩容阈值,然后定义新的容量newCap
和新的阈值newThr
. - 第一个判断:合法的扩容处理.
- 如果
oldCap>0
,则将新的容量和阈值都扩大为原来的2倍.- 如果已经扩到了
MAXINUM_CAPACITY
,则扩容阈值设置为Integer.MAX_VALUE
.不进行扩容.直接返回oldTab
. - 否则看一下是否到了最大容量
(newCap=oldCap<<1)
且设置新的阈值newThr=oldThr<<1
. - 如果都不是则不设置新的阈值.
- 如果已经扩到了
- 如果
oldThr>0
,表示我们在构造函数中设置了初始的大小.直接更新newCap=oldThr
即可. - 否则表示第一此初始化容器,使用默认参数构造
newCap
和newThr
.
- 如果
- 第二个判断:如果新的扩容阈值为空,表示当前table为空,但是有阈值.
- 计算新阈值
ft=newCap*loadFactory
. - 如果
新的容量小于2^30&&ft<2^30
.就将ft
赋值给newThr
.否则为Integer.MAX_Value
.
- 计算新阈值
- 我们获得了所有的更新后的容量和阈值之后.开始进行扩容.
- 申请一个新的容量的
newTab
,将其赋值给全局的table
. - 如果老数组不为空我们则需要进行赋值操作.
- 遍历每个老数组中每个位置的链表或者红黑树,重新计算节点位置,然后插入新数组.
- 对于每一个
oldTab
中存在元素的位置.我们先释放原数组中的元素. - 如果此时,该节点是一个独立的
(e.next==null)
,则直接计算新的桶中的下标(e.hash&*(newCap-1))
.插入其中. - 如果该节点为红黑树节点,则需要根据
split()
函数进一步确定该节点在新数组中的位置. - 否则就是拷贝链表.
- 定义了四个节点,分别是低位头尾和高位头尾.
为什么会有低位和高位之分?
我们对原数组进行扩容,我们的容量增大一倍,那么我们原来冲突的hash值现在可能不冲突了.分成了高位和低位两种情况. - 这里的
e.hash&oldCap
可以判断出该节点应存放的位置.等于0表示在低位,大于0表示放在高位.然后重新放入链表 - 将所有的元素都放完后,分别判断低位和高位的
Tail
.并且将其Head
放入newTab
中.
- 定义了四个节点,分别是低位头尾和高位头尾.
- 最后返回
newTab
.
可以看出来,扩容函数resize()
主要干了两件事:
- 获得一个新的合理的容量
newCap
和阈值newThr
. - 申请完新的
table
后进行值的拷贝.注意了高位和低位的分开.
2.4 修改
HashMap
中没有update等操作.我们可以通过put()
来修改.
2.5 删除
查看remove(Object key)
函数.调用removeNode()
函数,返回删除节点的value.
注意这里有两个remove()
.分别是remove(Object key)
和remove(Object key,Object value)
.观察发现其区别就在于调用removeNode()
函数中参数matchValue
和movable
.
matchValue
为true
表示只有当Value
和传入的value
相同时才删除节点.
movable
为true
表示删除
查看removeNode()
:
- 前半部分和我们的查找类似.显示找到目标节点
node
. - 接下来判断是否是我们要删除的节点.查看
matchValue
参数比较. - 如果是红黑树节点,调用
removeTreeNode()
函数. - 然后将这个节点删除,向后推一位.
- 更新
modeCount
和size
.
参考资料
HashMap源码分析:https://www.jianshu.com/p/2e2a18d02218
容器类框架分析-HashMap源码分析:https://www.jianshu.com/p/4f7add8ed8f5