HashMap源码分析
文章目录
一、HashMap成员变量
// HashMap的默认初始化容量,左移四位 = 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大的哈希表容量 左移30位 = 2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的负载因子 0.75,使用泊松分布算法根据时间与空间的利用,取折中值0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/*
桶中链表长度转换红黑树的阈值,链表长度超过8的时候,链表会转换为红黑树。
设置这个阈值的目的有两个:
1. 避免链表过长,查找效率降低。转换成红黑树可以提高查找效率。
2. 如果阈值过小,链表转换成红黑树的次数会更多,反而影响性能。阈值8是一个经验值。
*/
static final int TREEIFY_THRESHOLD = 8;
/*
当在resize时,若一个红黑树中的节点数少于该值,会将红黑树重新转换为链表。
这个阈值的设置也有两个考虑:
1. 避免红黑树规模太小,维护代价太大。
2. 阈值要小于TREEIFY_THRESHOLD,否则树化和退化会频繁转换。
所以UNTREEIFY_THRESHOLD的作用是设置红黑树转换回链表的阈值。退化可以避免红黑树太小的维护开销。
*/
static final int UNTREEIFY_THRESHOLD = 6;
/*
当桶中的节点数超过TREEIFY_THRESHOLD(默认为8)时,若当前table的大小< MIN_TREEIFY_CAPACITY,会优先进行扩容,而不是树化。
这个设定的考虑是:
1. 当表较小时,哈希冲突可以通过扩容来解决。
2. 避免表较小时就树化,造成红黑树过多,反而降低效率。
所以,MIN_TREEIFY_CAPACITY用于设定进行树化的最小容量阈值,与扩容配合来优化冲突。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/*
存储HashMap中的键值对数据。
table是一个Node<K,V>类型的数组,每个Node表示一个键值对。
*/
transient Node<K,V>[] table;
/*
记录当前HashMap中包含的键值对数量。
size字段用于快速获取Map中的个数,而不需要每次遍历计算,可以优化速度。
*/
transient int size;
/*
用于记录HashMap结构变化的次数,如put、remove等操作。主要用于迭代时的快速失败(fail-fast)机制。
*/
transient int modCount;
/*
用于存储HashMap的阈值,当HashMap中的键值对数量达到threshold时就会进行扩容resize操作。threshold = capacity * loadFactor。
*/
int threshold;
/*
该字段为HashMap的负载因子,用于控制HashMap的密度。loadFactor越大密度越高,冲突的机会加大。默认值为0.75,是在时间和空间成本上做的一个平衡选择。
*/
final float loadFactor;
二、构造方法
1. 无参构造方法
// 其他所有的参数都为默认
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
2. 有参构造方法
开放了4个有参构造方法,值得注意的是初始化容量的有参构造方法,不会将HashMap中的容量设定为传入的大小,会进过一系列的位运算后,将传入的容量大小计算为大于或等于传入容量大小的2的幂数值
// 输入初始容量的构造方法。这里的初始参数还需要进行计算为2的幂数值
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 输入初始容量、负载因子的构造方法。
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);
}
/*
计算出大于或等于输入capacity的最小的2的幂数值
原理是利用位运算实现的:
1. 将输入值cap减1,赋值给n。
2. 将n右移1位,然后与原n进行“或”运算。这将保证结果为偶数。
3. 重复右移并“或”运算,直到将所有的1位都置为1。这实际上是计算大于等于cap的最小的2的幂。
4. 如果计算结果小于0,直接返回1。如果结果大于最大容量,返回最大容量。否则返回n+1作为结果。
所以tableSizeFor()实际上是计算一个rounding operation,将输入数值上取到大于等于该值的最小的2的幂数。
这样可以保证HashMap的容量总是2的幂,这对于提高哈希的效率很关键。同时也符合HashMap的扩容机制。
*/
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;
}
// 传参为map的构造方法,先将传入的map中的成员变量计算到新的HashMap中,并且遍历传入的map,使用putVal方法将各个参数复制到新的HashMap中。
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
三、put方法
1. put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
2. hash方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap中的hash方法并不是单纯的使用hashCode()
方法直接返回对应的hash值,而是会将返回的hash值的高16位与hash值的低16位使用异或位运算进行扰动,使hash值更加的离散,提高散列性,减少hash冲突。
翻看源码可以提出以下问题:
- 为什么不直接使用hashCode()方法生成的hash值来使用,还要进行运算?
- 为什么要使用hash值的高16位于hash值的地16位进行扰动,或者说为什么是右移16位?
- 为什么要使用异或位运算扰动,而不使用与运算、或运算?
- 扰动之后为什么可以提高散列性?
-
针对第1、2的问题:
首先是hash值,hash值是int类型,4个字节,32位。所以低16位的变化区间在 0 ~ 2^16-1 这个区间,而高16位的变化区间在 -2^32 ~ 2^32-1 这个区间,那么参与计算的hash值是低16位参与的更多,如果不进行扰动,让高16位不参与运算,会导致散列程度不高。(例如两个hash值的低十六位是一样的,而高十六位不一样,如果直接计算的话,那么数组下标会相同,如果这时候高位与低位异动之后,高位也参与运算,这时候数据分布更加均匀)。所以需要将hash值的高16位与低16位进行运算、扰动,让高16位于参与到运算中,减少hash冲突,防止大量kv值都在一个个下标数组中。 -
针对第3的问题:
使用异或位(异或,相同为0,不同为1),由于右移16位之后的hash值高16位都为0(如果使用与运算,那么计算后的hash值高16位全为0,高16位特征全无),使用异或运算,计算之后的hash值会保留原来hash值的高16位特征。同时右移之后的低16位中有1则会参与异或运算中扰动,如果这时候使用的是或运算,当原hash值的低16位全为1时,那么运算之后的hash值相当于没有扰动成功,还是会出现hash冲突的情况。
综上所述,可以解答第4问。
3. putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 给tab赋值,如果当前数组为空或者长度位0,那么需要初始化数组,并返回数组的大小n
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算出当前要put的数据的数组下标,找到数组对应的元素位置,
// 并赋值给p,判断当前数组位置元素是否为空
if ((p = tab[i = (n - 1) & hash]) == null)
// 当前数据为空,那么将输入的数据生成新的node对象,赋值到对应的元素位置。
tab[i] = newNode(hash, key, value, null);
else {
// 如果当前数组存在元素的情况。
Node<K,V> e; K k;
// 如果当前数组位置上元素的hash等于传入数据的hash值,同时key相等。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 把原有的元素赋值给e,判断跳出,后续做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节点的下节点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 设置完节点之后,除去已设置的节点,当前链表的数量为binCount+1,如果大于或等于8,那么需要设置树节点
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 设置树节点,里面会判断该tab数组长度是否大于MIN_TREEIFY_CAPACITY=64,
// 如果大于才会将当前hash对应的节点设置为树节点,否则只会使用resize()方法进行扩容
treeifyBin(tab, hash);
// 跳出循环,这里e==null
break;
}
// 如果e(当前节点的下节点)不为null,且hash、key与当前e的相同,那么跳出循环。后续处理赋值操作
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 未赋值前,e为p的下节点,这时候将e复制给p,那么
p = e;
}
}
// e不为空情况:1.当前节点为树节点
// 2. 当前节点数据和传入的数据的数组下标相同以及key相同。
if (e != null) { // existing mapping for key
// 将旧节点的value值赋值给oldValue
V oldValue = e.value;
// onlyIfAbsent 为true的话,当前节点有value值,那么不会替换新value值
if (!onlyIfAbsent || oldValue == null)
// 将新的value值赋值给当前节点。
e.value = value;
afterNodeAccess(e);
// 返回旧节点中的value
return oldValue;
}
}
// 只有当链表、树节点增加或者删除了节点才会+1
++modCount;
// 新节点插入之后,size+1,且判断是否超过扩容阈值,超过那么需要进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
4. treeifyBin()方法
// 链表转红黑树方法
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果传入的节点数组为空,或者传入的节点数组长度小于64,那么会使用resize方法进行扩容。
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 根据hash找到对应的链表,不为null则开始树化。
TreeNode<K,V> hd = null, tl = null;
do {
// 遍历链表,将链表中每个节点元素都转换成树节点。
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
// 第一个树节点元素赋值给hd
hd = p;
else {
// 除了第一个树节点元素,其他树节点设置前一节点、后一节点,组成双向链表。
p.prev = tl;
tl.next = p;
}
// 将当前树节点p赋值给tl,未赋值前tl为前一树节点。
tl = p;
} while ((e = e.next) != null);// 节点元素下一节点不为null,继续循环。
// 将设置好的红黑树链表hd赋值到节点数组中。覆盖原来的链表。如果不为空,还需要对红黑树节点组织成红黑树结构。
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
四、 resize数组初始化、扩容
1. resize()
final Node<K,V>[] resize() {
// 获取当前节点数组,复制给oldTab为老数组
Node<K,V>[] oldTab = table;
// 设置老节点数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 设置老节点数组的阈值。
int oldThr = threshold;
// 初始化新数组长度、新数组阈值
int newCap, newThr = 0;
// 如果老数组的长度大于0
if (oldCap > 0) {
// 如果老数组长度大于等于节点数组的最大值 2^30
if (oldCap >= MAXIMUM_CAPACITY) {
// 那么老数组阈值设置为int的最大值 2^32-1
threshold = Integer.MAX_VALUE;
// 并返回老节点数组
return oldTab;
}
// 如果老数组长度左移一位,即两倍,赋值给新数组(这里相当于将数组长度扩容了2倍)
// 并且小于数组长度最大值以及老数组长度大于默认的16.
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 老节点数组阈值长度左移一位,即两倍,然后复制给新数组阈值。(数组扩容两倍、阈值扩容两倍)
newThr = oldThr << 1; // double threshold
}
// 如果老数组长度=0且老数组阈值大于0
else if (oldThr > 0) // initial capacity was placed in threshold
// 新节点数组长度就等于老数组的阈值
newCap = oldThr;
// 老数组长度、阈值都为0
else { // zero initial threshold signifies using defaults(这里是初始化数组)
// 初始化数组长度为默认的16
newCap = DEFAULT_INITIAL_CAPACITY;
// 初始化新数组阈值为默认负载因子0.75X默认初始化容量大小16 = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新数组阈值等于0
if (newThr == 0) {
// 那么新数组长度X负载因子得到ft(初始阈值)
float ft = (float)newCap * loadFactor;
// 根据计算出来的新数组初始阈值判断,如果新数组长度、计算得到的初始阈值小于容量最大值,那么新数组阈值为初始阈值ft
// 否则新数组阈值为int的最大值 2^32-1
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 将新数组阈值复制给当前HashMap阈值属性。
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 根据计算出来的新数组大小创建一个新的节点数组。
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 创建出来的新节点数组赋值给table。
table = newTab;
// 如果老数组不为null,这里需要将老数组中的元素转移到新数组中。
if (oldTab != null) {
// 遍历老数组上的节点元素
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 把当前下标数据赋值给e,数组上某一下标有数据,那么开始转移数据到新数组中。
if ((e = oldTab[j]) != null) {
// 当前下标的数据设置为0。
oldTab[j] = null;
// 判断当前节点数据的下节点是否为空,为空就说明当前节点是单一数据,不是链表。
if (e.next == null)
// 单一节点数据的hash与新数组长度-1进行与运算,算出该节点数据在新数组的下标,并赋值。
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。
next = e.next;
// 低位节点、高位节点判断:例如现在老数组长度16,那么计算下标的时候就是hash & (16-1),
//这时候节点只有低4位参与与运算,能够组成链表,那么说明该链表上的节点hash低4位都是一样的。
// 这时候数组长度翻倍变成32,那么计算节点在新数组的下标就变成hash & (32-1),
// 这时候就是低5位参与与运算,由于低4位都是一样的,所以节点上第低5位为1,
// 则在新数组下标有变化(还可以知道当前变化位刚好就是老数组长度,所以变化的下标为老数组下标+老数组长度),为0则下标无变化。
// 可以反推,只有低第5位有变化,那么在新数组下标有变化,所以hash与老数组长度为0,那么无变化,为低位节点。
// 如果当前节点数据hash与老数组长度进行与操作等于0,
// 那么说明当前节点数据在新数组的下标和老数组的下标是一样的,那么设置为低位节点。否则为高位节点。
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);// 判断下一节点不为null,那么继续循环。
if (loTail != null) {
// 低位尾节点不为null,那么在新数组下标和老数组下标一致,将节点赋值到新数组对应的下标节点中。
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
// 高位尾节点不为null,那么在新数组下标是老数组下标+老数组长度。然后将节点赋值到新数组对应的下标节点中。
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
2. split()方法,拆分树节点
// map当前hashmap
// tab:老节点数据数组
// index:老节点数组下标
// bit:老数组容量大小
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
// 将当前树节点设置为b
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
// 设置低位头树节点、尾树节点;高位头树节点、尾树节点。
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
// 设置低位树节点长度lc,高位树节点长度hc
int lc = 0, hc = 0;
// 遍历树节点
for (TreeNode<K,V> e = b, next; e != null; e = next) {
// 设置当前树节点e的下树节点为next。
next = (TreeNode<K,V>)e.next;
// 设置e的下树节点为null
e.next = null;
// 当前树节点hash与老数组大小进行与运算,如果为0,说明为低位树节点。
if ((e.hash & bit) == 0) {
// 如果当前树节点的上树节点为null
if ((e.prev = loTail) == null)
// 那么设置当前树节点为低位头树节点。这时候低位头树节点、尾树节点都是e
loHead = e;
else
// 设置当前树节点e为低位尾树节点的下树节点。(这里开始拼接树节点链表。)
loTail.next = e;
loTail = e;
++lc;// 计算低位树节点链表的长度。
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
// 如果低位树节点不为null
if (loHead != null) {
// 判断低位树节点长度是否小于或等于树节点退化长度6
if (lc <= UNTREEIFY_THRESHOLD)
// 那么将当前树节点链表遍历设置为节点node后赋值到新数组对应下标中。
tab[index] = loHead.untreeify(map);
else {// 当前树节点长度大于树节点退化长度6.
// 直接将对应的树节点赋值到新数组的对应下标中。
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
// 这里判断高位树节点是否为null,如果不为null,说明低位树节点的不是原来的整条树节点,有变化,所以需要重新生成树结构。
loHead.treeify(tab);
}
}
// 与低位树节点同,不同的是对应的新数组下标+老数组长度。
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}