目录
前言
原码分析
继承关系
类中属性
构造函数
核心方法
总结
附录1 HashMap与HashSet关系?
附录1 HashMap初始化如何保证容量是2的幂?
附录3 HashMap 如何计算节点所在数组(桶)的位置
附录4 HashMap 扩容时元素位置如何迁移?
基于哈希表的 Map 接口的非同步实现,此实现提供所有可选的映射操作,并允许使用 null 值和 null 键,不保证映射的顺序。另外,HashMap的数据结构是数组+链表/红黑树。
接下来我们针对其原码做以下分析:继承关系、类中属性、构造函数、核心方法 四个方面分析
public class HashMap
extends AbstractMap implements Map , Cloneable, Serializable {
- Map: 接口用于存储元素对(称作“键”和“值”),其中每个键映射到一个值
- AbstractMap:实现了Map接口部分方法,为map各自子类实现公共的方法
- Cloneable:支持接口,实现
Cloneable
接口,支持Object.clone()方法(CloneNotSupportedException
)- java.io.Serializable:标记接口,支持序列化
- DEFAULT_INITIAL_CAPACITY:缺省的初始化容量16
- DEFAULT_LOAD_FACTOR:缺省的负载因子常量0.75
- MAXIMUM_CAPACITY:最大容量(int型只能左移动30位,31位就变为负数的了)
- TREEIFY_THRESHOLD:链表节点数大于8个链表转红黑树
- UNTREEIFY_THRESHOLD:链表节点小于6个红黑树转链表
- MIN_TREEIFY_CAPACITY:当数据大于等于64的时候才触发链表转红黑树
- loadFactor:负载因子
- threshold:临界值 = 容量 * 负载因子
- size:映射中存在的键值对的数量
- entrySet:具体元素存放集
- table:以Node
数组存储元素,长度为2的次幂 - modCount:多线程并发修改触发fail-fast机制
接下来我们从初始化、新增、修改、查询、删除分析下原码
初始化 Map
hashMap = new HashMap<>();
新增 hashMap.put(key,value)
1)根据key计算hash值
2)判断当前hashMap集合的数组tab[]是否为空;如果为空则执行resize()扩容。
3)根据hash值计算数组的位置i,求得数组tab[i]的值;如果tab[i]=null新建节点并添加到tab[i]并跳转到4),否则跳转到a)
a) tab[i]的首个元素是否和key相等跳转到d),否则继续执行
b) tab[i]是否为红黑树,如果是执行红黑树的元素的插入操作并跳转到d),否则继续
c) 遍历tab[i]直到找到相同的key,或者遍历节点的next为null,在此节点尾插入新增节点
d) 判断节点是否新增节点还是修改节点,如果是修改节点则替换原值并跳转到7),否则跳转到4)。
4) 修改计数器modCount+1
5) 修改集合元素大小size+1
6) 判断size>threshold(扩容阀值),满足条件扩容,否则结束
7) 结束流程
扩容步骤:
- 根据原容量的值是否>0计算新容量和扩容阀值并跳转到4),不大于0则继续
- 根据原阀值是否>0计算新容量和扩容阀值并跳转到4),不大于0则继续
- 初始化默认的容量(16)和阀值(16*0.75)
- 判断新阀值是否为0,为0则重新计算新阀值
- 创建新数组(新容量大小)
- 迁移老数组数据至新数组
public V put(K key, V value)方法:
public V put(K key, V value) {
//hash(key)根据key计算hash值;在下文中用hash值计算key分布在数组的位置
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// tab-hashMap的数组; p-存放(k,v)的节点;n-数组长度;i-数组索引
Node[] tab; Node p; int n, i;
// tab为空说明数组还未初始化,HashMap未保存过数据,故需要扩容resize()
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//根据hash值计算key保存在数组的位置i;tab[i]=null,此处无其它元素,直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//数组位置已经存在其它元素,接下来从链表、红黑树等添加元素
Node e; K k; //e-需要添加的元素;k-需要添加元素k
//链表的首个元素与需要添加key相等;把当前元素p赋值给e,后面替换其键值对value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//当前元素对应的结构为红黑树结构,执行树的新增操作
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
//链表的首个元素非需要添加的key,循环遍历单向链表
for (int binCount = 0; ; ++binCount) {
//遍历整个链表均无相同的key,新增元素节点并添加到链表末尾并退出
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果链表长度>6,考虑链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//循环遍历链表过程中发现存在的key,则退出等待后面的键值对value替换值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//循环遍历链表时,当前节点后移
p = e;
}
}
//e不为空说明待添加的key存在,需要替换value;并退出整个方法,不修改modCount,size
if (e != null) { // existing mapping for key
V oldValue = e.value;
// onlyIfAbsent为ture代表元素缺少或者value为null才添加
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//修改计数器
++modCount;
//size+1,并判断是否需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
扩容方法:resize()
final Node[] resize() {
// oldTab = 扩容前hashMap的数组结构
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
// newCap 需要扩容的容量; newThr扩容阀值(临界值)
int newCap, newThr = 0;
//原容量>0说明原hashMap已经存在元素,一般扩容为其2倍
if (oldCap > 0) {
//如果原容量已经是最大值,仅调整临界值为Integer的最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 扩容后容量为其原容量的2倍(newCap = oldCap << 1)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//扩容阀值扩容为其原阀值2倍
newThr = oldThr << 1; // double threshold
}
//原容量<=0但原阀值>0,新容量=原阀值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//原容量、原阀值均<=0,HashMap为初始化状态,容量和阀值扩容为缺省值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
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;
//根据新容量创建HashMap创建扩容后需要的数组
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//遍历原数组赋值;由于是扩容为2倍且为2的幂;扩容后的位置为其原位置或者原位置+原容量
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
//原数组的额j位置置为null,垃圾回收释放原数组空间
oldTab[j] = null;
if (e.next == null)
//原数组当前位置只有一个值,计算扩容后位置并赋值
//扩容后的此位置也仅有一个元素
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//数组所在位置的元素为红黑树,执行树的扩容操作,涉及红黑树转链表
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order
//原数组所在的位置有多个元素(单向链表),遍历链表
Node loHead = null, loTail = null; //扩容后位置不变
Node hiHead = null, hiTail = null; //扩容后位置+原容量
Node next;
do {
next = e.next;
//扩容后位置不变(思考下2的幂bit位只有一个1)
if ((e.hash & oldCap) == 0) { //结果只有0和oldCap
if (loTail == null)
//数组当前位置首次添加loHead=loTail=e
loHead = e;
else
//数组位置非首次添加,后来元素添加到前一个元素末尾成单向链表
loTail.next = e;
//loTail始终定位到正在遍历的元素,为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;
//当前位置+oldCap添加扩容后新组装的链表
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
- 关于红黑树的操作,后期单独分析
- 扩容后元素位置为原位置或者原位置+原容量下文详细分析
修改 hashMap.replace(key,value)
1)调用hash()方法计算key的hash值
2)调用getNode()获得需要替换的节点(参数为上一步的计算的hash值和key)
a)根据hash值计算查询节点所在数组位置;并获得当前位置的的首个元素first
b) first的key与需要查询的key相等return first,否则继续
c) first数据类型为红黑树,查询红黑树的节点返回,否则继续
d) 遍历first查找相同的key返回,或者返回null
3)节点不为null,替换当前节点的value并返回原value;节点为null退出return;
查询 hashMap.get(key)
删除 hashMap.remove(key)
A) 同getNode()方法类似根据key和其hash值定位删除的节点,如有则继续
B) 删除对应的节点node
C) 删除后改变对应的数组或者链表或者红黑树的连接
D) 修改计数器modCount+1;集合的容量size-1;
E) 返回需要删除的节点
remove()方法
removeNode()方法
- HashMap实现了Map接口对键值对进行映射, 并允许使用null值和null键,但中不允许重复的键
- HashMap数据结构为数组+链表/红黑树(链表长度>8且数组长度>=64转换为红黑树)
- HashMap 的实例有两个参数影响其性能:初始容量 和加载因
- HashMap采用了数组和链表的数据结构,能在查询和修改方便继承了数组的线性查找和链表的寻址修改
HashSet底层实现是HashMap
; v 是固定对象PERSENT
初始化时调用方法tableSizeFor()保证容量为2的幂:
n |= n >>> 1 等价于 n = n | n>>>1 ; 接下来主要分析下n|n>>>1运算结果
- 首先二进制bit位仅且仅0、1;
- 桶容量如果为2的幂,二进制仅有一个1(例:01=1,10=2,100=4,1000=8,….)
- n>>>1相当于n的值无符号右移动1位,高位补0
我得到们接下来实际演示一下具体的例子就能了解其原理:(见下图)
- 目的确保左侧第一个1后面的位均为1
- 操作完成最后获得的n再+1(n+1)后,原数的最左侧1进一位,且后面bit位均为0
下图的n为001*….经过运算后001后面的所有位均为1(0011 1111 1111 1111 1111 1111 1111 1111);
执行n+1后的结果为:0100 0000 0000 0000 0000 0000 0000 0000
计算key所在数组的下标经过2步骤:
- Step1:hash()方法计算key的hash值
- Step2:根据hash值和数组长度计算key所在数组位置(hash值以数组长度为模)
hash()方法通过key对应hashCode的低16位与高16位异或操作得到key的hash值
hash & (数组长度-1) 获得key对应的数组位置即为取模操作;理解稍费劲,接下来我们看下具体的例子:
例如:数组的长度(桶容量)为8 (二进制:1000) ;
- 8-1=7的二进制位0111
- hash & 0111只看后3位(二元位操作符&代表2个运算的数都是1则为1否则为0)
- hash & 0111相当于与8取模(结果在[0-7]之间)
十进制 |
hash值(二进制) |
桶容量(8) - 1 |
hash&(cap-1) |
数组位置 |
1 |
0001 |
0111 |
0001 |
1 |
2 |
0010 |
0111 |
0010 |
2 |
8 |
1000 |
0111 |
0000 |
0 |
9 |
1001 |
0111 |
0001 |
1 |
10 |
1010 |
0111 |
0010 |
2 |
16 |
1 0000 |
0111 |
0000 |
0 |
17 |
1 0001 |
0111 |
0001 |
1 |
18 |
1 0010 |
0111 |
0010 |
2 |
32 |
10 0000 |
0111 |
0000 |
0 |
33 |
10 0001 |
0111 |
0001 |
1 |
34 |
10 0010 |
0111 |
0010 |
2 |
hashMap扩容后原节点扩容后位置在原位置 or 原位置+原容量,当hash & 原容量=0则在原位置,否则在原位置+原容量;引起这个的原因主要是由于①容量为2的幂 ②扩容后容量为原容量的2倍(计算公式为:hash & 原容量=0或者原容量)
首先我们先看下hash & 原容量的值为0或者原容量,为什么?因此原容量是2的幂且二进制表示的时候仅有一位bit位为1,与其它数按位&操作,要么全为0,要么跟原容量相等。
解决了上一个问题,那为什么节点扩容后位置为原位置或者原位置+原容量呢?其实这个原因主要还是我们HashMap扩容量为其原来的2倍(二进制位表示的话为原容量左移1位),接下来我们分析下一组数据即可找到原因,例如原容量为8现在扩容为16(你会发现数组位置16个数一个循环[0-15])
Hash值 |
桶容量为8 |
桶容量为16 |
|
|||||
十进制 |
二进制 |
cap - 1 |
hash&(cap-1) |
数组位置 |
cap- 1 |
hash&(cap-1) |
数组位置 |
hash&原cap (8) |
1 |
0001 |
0111 |
0001 |
1 |
0 1111 |
0001 |
1 |
0 |
2 |
0010 |
0111 |
0010 |
2 |
0 1111 |
0010 |
2 |
0 |
8 |
1000 |
0111 |
0000 |
0 |
0 1111 |
1000 |
8 |
1 |
9 |
1001 |
0111 |
0001 |
1 |
0 1111 |
1001 |
9 |
1 |
10 |
1010 |
0111 |
0010 |
2 |
0 1111 |
1010 |
10 |
1 |
16 |
1 0000 |
0111 |
0000 |
0 |
0 1111 |
0000 |
0 |
0 |
17 |
1 0001 |
0111 |
0001 |
1 |
0 1111 |
0001 |
1 |
0 |
18 |
1 0010 |
0111 |
0010 |
2 |
0 1111 |
0010 |
2 |
0 |
22 |
1 0110 |
0111 |
0110 |
6 |
0 1111 |
0110 |
6 |
0 |
24 |
1 1000 |
0111 |
0000 |
0 |
0 1111 |
1000 |
8 |
1 |
32 |
10 0000 |
0111 |
0000 |
0 |
0 1111 |
0000 |
0 |
0 |
33 |
10 0001 |
0111 |
0001 |
1 |
0 1111 |
0001 |
1 |
0 |
34 |
10 0010 |
0111 |
0010 |
2 |
0 1111 |
0010 |
2 |
0 |