HashMap是一个基于key做hash的K-V数据结构。
这篇文档会用通俗易懂的方式解读一下HashMap的源码, 看看HashMap底层到底是咋回事。
话不多说, 开整。
先把HashMap的核心结构整出来, 下面标注出了每一个属性和方法都是干啥的, 盘它~
public class HashMap<K,V>...{
transient Node<K,V>[] table; // 存储数据的数组 (主要就是操作这个玩意)
transient Set<Map.Entry<K,V>> entrySet; //遍历用的全量数据Set
transient int size; //数组的大小
transient int modCount; //修改计数器。fail-fast用的
int threshold; //阈值, 数组长度达到这个值, 就会考虑扩容
final float loadFactor; //负载因子
// 复载了三个构造方法
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 默认0.75
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException(...);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException(...);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
//查
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//批量新增
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
//增+改
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//删
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
//扩容
final Node<K,V>[] resize() {
...
}
//树化
final void treeifyBin(Node<K,V>[] tab, int hash) {
...
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
}
大体结构, 就是一个Node数组, 用来存K-V。
然后有entrySet/size/modeCount/htreshold/loadfactor这几个属性。
以及三个构造和hash、增、删、改、查、扩容、树化等方法。
还算简单明了。
下面我们主要看一下hash、put、扩容 其他的方法有时间再整理给大家
先从hash方法开始
HashMap为了提升hash效率主要做了两个优化:
- 使用位运算&代替取模(%)运算来计算下标。
- 对key的hashcode进行扰动计算。
下面是位运算&计算下标的源码, 我们分析下实现原理。
1、位运算&代替取模(%)运算来计算下标
(n - 1) & hash
n是数组长度, 假设n = 16, hash = 51, 那么下标 = hash & 15 = 3 = 51 % 16
原理是:
在二进制计算下
hash/16相当于 hash >> 4, 被移除的4位刚好就是余数 hash % 16。
而hash & 15刚好取的就是最后4位。
所以可以讲 hash & 15 等价于 hash % 16。
而且这个方法只对2的n次幂起作用, 换成其他数就不开门了~
所以HashMap初始长度是2的N次幂, 每次扩容也都是2倍的扩。
2、扰动计算
在hash方法中, 高16位和低16位做了个抑或运算, 这里就是扰动计算的实现。为啥这么干呢?
static final int hash(Object key) {
int h;
// h ^ (h >>> 16)
// 高16位和低16位做了个抑或运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
我们用大一点的数字举例, 可以看到, 高位上的数字其实是被抹掉了, 没有参与到运算中, 这样会导致, 高位不同低位相同的数发生碰撞, 增大了碰撞的概率, 所以将高低位进行抑或, 这样高低位就都参与到下标计算中, 减小了碰撞和概率。
看下put方法流程
public V put(K key, V value) {
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;
// 源码写法自带防御属性, 我们改一下, 把他拍散, 方便加注释, 辅助阅读
Node<K,V>[] tab = table; // 就是table数组
int n = table == null ? 0 : tab.length; //table数组的长度
int i = (n - 1) & hash; // 当前key应该存放位置的角标
Node<K,V> p; // 当前key将要存放位置链表/红黑树上的Node节点(遍历用)
if (tab == null || n == 0)
// 欧吼~ 懒加载哈
// 数组没有初始化, 那就去初始化, resize()是初始化或者扩容
n = (tab = resize()).length;
if ((p = tab[i]) == null)
// 要插入的位置上没有元素, 直接将K-V包装成Node存进去
tab[i] = newNode(hash, key, value, null);
else {
// 否则, 说明发生hash碰撞
Node<K,V> e; // 撞上的那个节点(会遍历赋值, 判断是否要覆盖)
K k = (p == null ? null : p.key);// 撞上的那个节点的key
if (p.hash == hash && (k == key || (key != null && key.equals(k)))){
//本次要存的key和链表/红黑树上第一个节点的key相等, 说明重复了, 后面需要做value覆盖
e = p;
}else if (p instanceof TreeNode){
//红黑树节点, 遍历红黑树 插入到红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
} else {
//链表节点, 遍历链表 插入到链表
for (int binCount = 0; ; ++binCount) {
//处理遍历到的当前节点
e = p.next;
if (e == null) {
// 当前节点为null, 说明遍历完了。就把当前k-v封装成Node, 追加到链表尾部
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1){
// 链表长度 > 8 进行树化
treeifyBin(tab, hash);
}
break;
}
//当前节点key与本次要存的key相等, 说明重复了, 后面需要做value覆盖
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){
break;
}
// 指针往后移动, 指向下一个节点
p = e;
}
}
if (e != null) {
// 发生碰撞, 且key已经在Map中, 进行value覆盖
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null){
e.value = value;
}
//模板方法, 留给子类扩展
afterNodeAccess(e);
return oldValue;
}
}
//计数器, fail-fast是哟哦那个
++modCount;
if (++size > threshold){
//超过阈值, 扩容
resize();
}
//模板方法, 留给子类扩展
afterNodeInsertion(evict);
return null;
}
总结一下就是:
1、先通过key的hash计算索引位置
2、判断该位置为null, 就直接将K-V封装成Node节点存进去
3、如果不为空, 说明有碰撞, 遍历链表, 做K值比较, 如果找到相等的key, 那就将该节点的value覆盖
4、如果没有找到相等的key, 那就存到链表尾部, 然后判断是否需要树化
5、++size后判断是否需要扩容, 若大于阈值就进行扩容
扩容逻辑
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//原数组
int oldCap = (oldTab == null) ? 0 : oldTab.length;//元数组大小
int oldThr = threshold;//扩容阈值
int newCap = 0;//新数组大小
int newThr = 0;//新扩容阈值
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
//老数组大小 达到最大值 不在继续扩容
threshold = Integer.MAX_VALUE;
return oldTab;
}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY){
//向左位移1 相当于 *2
//新数组长度 < 最大值 并且 老数组长度 > 默认值 阈值*2
newThr = oldThr << 1; // double threshold
}
} else if (oldThr > 0){
newCap = oldThr;
}else {
//初始化
newCap = DEFAULT_INITIAL_CAPACITY;//默认16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//默认12
}
if (newThr == 0) {
//新数组的阈值
float ft = (float)newCap * loadFactor;
//新数组长度 < 最大值。 新阈值 < 最大阈值。 就是用上一行计算的阈值, 否则用最大阈值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//新建数组
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){
// 只有一个节点, 未形成链表, 只需要将当前节点重新hash到新数组中
newTab[e.hash & (newCap - 1)] = e;
} else if (e instanceof TreeNode){
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
} else {
//存在链表, 遍历链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
//遍历
next = e.next;
//当前node的hash与老数组长度取与
if ((e.hash & oldCap) == 0) {
//结果=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;
//换位置的节点 直接放进 新数组 j + oldCap位置上
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
扩容时, 遍历链表进行迁移, 用了 (e.hash & oldCap) == 0 这个判断, 啥意思呢?
如下图, 当数组长度从16扩大到32的时候, hash & oldCap = 0 的还在原来的位置, 而不等于0 的统一都向钱移动了oldCap的长度,
所以用这种方式就可以简单快速的将链表拆分成2部分, 一部分待在原来的位置, 另一部分移到新的位置, 不需要新老位置都进行遍历插入, 效率得到了很大提升。