HashMap源码分析
孔子曰:学而时习之,不亦说乎。
我还是比较喜欢不断的去推翻自己的结论的。每次都有新收获。欢迎大佬指出文中不足之处,小刘还在成长,一定虚心学习。
常量的含义
/**
* 默认初始化容量大小
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认负载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
*树化阈值
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 解除树化阈值
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 最小树化阈值
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* Node数组桶
*/
transient Node<K,V>[] table;
/**
* map大小
*/
transient int size;
/**
* map结构修改次数
*/
transient int modCount;
/**
* 扩容阈值
*
* @serial
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;
/**
* 负载因子,没什么好说的
*
* @serial
*/
final float loadFactor;
HashMap的几种构造方法
HashMap共有四种构造方法,主要讲一下第一种:指定初始化容量和负载因子,注意(真正的容量和阈值初始化放在了putVal()时进行)。
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
使用该构造方法时,需要传递两个参数,第一个是指定的初始化值,第二个是默认的负载因子。
接着看方法内部,首先对传递的初始化容量进行判断,如果初始化容量小于0则抛出异常。若初始化容量大于最大容量,就将初始化容量设置为最大容量,(不能再大了)。第三个判断,对负载因子进行判断处理,如果负载因子小于0或者传递的参数不是数字,抛出异常。
判断进行完,就要对变量进行赋值了,一个是负载因子,一个是扩容阈值。
在对扩容阈值进行赋值之前,要执行tableSizeFor(initialCapacity)的方法,这个方法的作用就是根据初始容量计算出扩容阈值,这个扩容阈值留到后面有用,小伙伴们可以注意一下,来看一下这个方法。
/**
* Returns a power of two size for the given target capacity.
* 返回一个大于等于cap的数,这个数一定是2的n次方
*/
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;
}
如果传入的cap正好是2的n次方,返回结果还是它本身,如果传入的cap不是2的n次方,就会返回一个大于它并且是2的n次方的数。
举个例子吧,比如传的cap为15
cap = 15
n = cap-1 = 15-1 => 14
n的二进制为 0b1110
n |= n >>> 1
0b1110 | 0b0111 = 0b1111
n |= n >>> 2
0b1111 | 0b0011 = 0b1111 => 15
到此为止就可以了,因为再往下都是15了。
也就是说下次扩容的阈值是n+1=16,那么为什么最开始要把cap-1呢,是因为如果不减1,那么最后返回的扩容阈值会是传入的cap参数的二倍。
put()和putVal()方法的解析
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
先看一下方法内的hash(key)。
想要将一个键值对插入到HashMap中,首先需要根据key来算出hashCode,再通过哈希扰动后,得到桶数组的下标,再将值插入到对应下标,也就是(table.length-1)&hash。
假设:h=hashCode():0b 1101 0100 1110 1010 0111 0100 0101 1011
h >>> 16 : 0b 0000 0000 0000 0000 1101 0100 1110 1010
进行异或运算后,hash值为:
1101 0100 1110 1010 1010 0000 1011 0001
而桶数组长度只能取2的n次方,假设长度为16,根据算法,插入下标为
n = table.length-1 :0000 0000 0000 0000 0000 0000 0000 1111
hash = 1101 0100 1110 1010 1010 0000 1011 0001
只算最后四位,0011 = 3,得到桶数组下标为3。
那么为什么要对key的hashCode进行高16位运算呢,这是因为如果数组table的length比较小的时候,也能保证其参与到Hash的计算中。
/**
* 扰动函数
* 作用:让key的hashCode参与高16位运算
*/
static final int hash(Object key) {
int h;
// 返回hash值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
回到put方法中,再来看一下putVal()方法,比较长,一步步分析。
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// tab:引用当前hashmap的散列表
// p: 表示当前散列表的元素
// n: 表示散列表数组的长度
// i: 表示路由寻址结果
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 延迟初始化,为了避免创建时占用内存空间而设计的。
// 第一步:使用table对tab进行赋值,并判断tab是否为空,长度是否为0
// 若为空,则对n进行赋值
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 插入判断
// 第一步:判断当前要插入的位置是否为空,如果为空,直接调用newNode生成一个新桶数组进行插入。若不为空,进行下一步判断。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// e: 不为null的话,找到了一个与当前插入的key-value一致的key元素
// k: 表示临时的一个key
Node<K,V> e; K k;
// 第二步插入判断:
// 如果当前被插入的数组位置上有元素,并且数组元素的hash值与待插入的hash值相等,而且key的值也相等,或者待插入的key不为空,并且key值等于数组中的key值
// 就将数组中的元素赋值给e
// 这一步要进行的就是同一个key并且hash值相同下的值覆盖操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断当前散列表是否为树形结构,这个以后再讲
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 根据HashMap的存储结构可以知道,这一步是对HashMap的桶数组中的链表结构进行判断,判断链表中是否有和待插入元素相匹配的。
// 定义for循环,循环链表
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;
}
// 如果p的next元素e的哈希值等于待插入元素的哈希值,并且e的key等于待插入元素的key,或者key不为空并且待插入元素和链表的两个key相等
// 则进行值的覆盖操作
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 值覆盖方法
if (e != null) { // existing mapping for 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;
}
扩容方法resize()。
HashMap的扩容呢,其实就是创建一个新的桶数组,将旧数组的值移动到新数组中。
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
// oldTab代表旧数组。
Node<K,V>[] oldTab = table;
// 旧数组的容量,若旧数组为空,则代表该hashmap还没有使用过,所以数组容量为0,否则为数组的长度。
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// oldThr:旧数组的扩容阈值
int oldThr = threshold;
// 扩容后table数组的容量和扩容阈值
int newCap, newThr = 0;
// 下面这个长长的if判断呢,就是给newCap和newThr赋值的一个过程。
// 如果旧数组容量大于0,证明含有数据,是一次正常的扩容。
if (oldCap > 0) {
// 判断旧数组容量是否大于最大值
if (oldCap >= MAXIMUM_CAPACITY) {
// 如果成立,就将扩容阈值赋值为Integer的最大值,也就是2的31次方减一,非常大的一个数,不可能再次扩容了
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 如果旧数组容量没有大于最大值,就将旧数组的大小左移一位并赋值给新table数组的大小,新数组的大小为旧数组的2倍
// 并且旧数组的大小大于等于默认初始化大小16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新扩容阈值是旧的2倍
newThr = oldThr << 1; // double threshold
}
// 还记得我在上面说过HashMap的初始化被推迟了吗。
// new HashMap(initCap,loadFactor);
// new HashMap(initCap);
// new HashMap(map);并且map中有数据
else if (oldThr > 0) // initial capacity was placed in threshold
// 将旧table数组的扩容阈值作为新数组的大小
newCap = oldThr;
else { // zero initial threshold signifies using default
// 不指定初始化大小的,那么也不会有扩容阈值
// 就将默认初始化大小赋值给newCap
newCap = DEFAULT_INITIAL_CAPACITY;
// 负载因子*大小=扩容阈值。
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// oldCap==0,oldThr==0
// new HashMap();
if (newThr == 0) {
// newCap=16
// ft = 16 * 0.75
float ft = (float)newCap * loadFactor;
// 如果newCap小于最大容量,并且ft也小于最大容量
// 就将ft赋值给newThr,否则为Integer最大值。
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 根据newCap,创建出一个更长,更大的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 将table数组指向newTab
table = newTab;
// 说明hashMap本次扩容前,oldTab不为null
if (oldTab != null) {
// 需要循环来处理旧数组的数据
for (int j = 0; j < oldCap; ++j) {
// 当前node节点
Node<K,V> e;
// 说明旧数组中有数据,并赋值给node节点e
if ((e = oldTab[j]) != null) {
// 将oldTab置空,方便GC回收
oldTab[j] = null;
// 表明当前node节点不是链表
// 第一种情况: 当前桶位只有一个元素,从未发生过碰撞
if (e.next == null)
// e.hash & (newCap-1) 寻址公式
// 将当前node节点赋值给新数组
newTab[e.hash & (newCap - 1)] = e;
// 如果当前node节点是树结构
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 如果为链表的话,执行到这步
else { // preserve order
// 低桶位链表
Node<K,V> loHead = null, loTail = null;
// 高桶位链表
// 这里就和hashMap的长度有关系了,当长度低时,部分key的hash值只有开始的四位能参与寻址运算。当长度达到一定程度时,第五位参与运算,那么部分第五位参与运算的数值的寻址结果就会发生变化。
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 如果e的hash值与oldCap运算为0,说明e节点在扩容后为低桶位链。
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;
}//这里可以参考一下美团的一篇文章,写的非常详细
// https://zhuanlan.zhihu.com/p/21673805
} 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;
}
剩下要讲的两个方法都非常简单,一看就懂
获取容器中的节点
/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
// tab表示当前散列表
// first:桶位中的头元素
// e:临时元素
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) {
// 表明当前桶内有数据,并且要查找的数据不为null
// 第一种情况,定位到的元素即为要get的元素
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 第二种情况,红黑树
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;
}
/**
* Implements Map.remove and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to match if matchValue, else ignored
* @param matchValue if true only remove if value is equal
* @param movable if false do not move other nodes while removing
* @return the node, or null if none
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
// tab表示当前散列表
// p表示路由寻址结果对应的当前node节点
// n表示散列表长度
// index:下标
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 如果当前散列表不为空并且长度大于0并且p不为空,则进入内部判断
// 说明桶内是有数据的,进行查找并删除
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
// node:查找到的结果
// e为当前节点的下一个元素
Node<K,V> node = null, e; K k; V v;
// 第一种情况,最理想的情况,当前桶元素正好为要删除的元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 说明当前桶位,要么是红黑树,要么是链表
if (p instanceof TreeNode)
// 第二种情况,红黑树查找
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 第三种情况
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//判断node不为空的话,说明按照key查找到需要删除的数据了
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//第一种情况:node是树节点,说明需要进行树节点移除操作
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//第二种情况:桶位元素即为查找结果,则将该元素的下一个元素放至桶位中
else if (node == p)
tab[index] = node.next;
//第三种情况:将当前元素p的下一个元素 设置成 要删除元素的 下一个元素。
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}