一 成员变量
核心成员变量
成员变量名 | 类型 | 作用说明 |
---|---|---|
transient Node<K,V>[] table | Node<K,V>[] | 存储键值对的哈希桶数组,是 HashMap 的主要数据结构,首次使用时延迟初始化 |
transient Set<Map.Entry<K,V>> entrySet | Set<Map.Entry<K,V>> | 缓存的 entrySet() 结果,用于 Map 的遍历或迭代 |
transient int size | int | 当前 Map 中键值对的数量 |
transient int modCount | int | 结构修改计数器,支持 fail-fast 机制,迭代时检测并发修改 |
int threshold | int | 扩容阈值,等于 capacity * loadFactor ,决定何时触发扩容 |
final float loadFactor | float | 加载因子,控制容量使用率,默认是 0.75f |
常量成员变量
常量名 | 类型 | 默认值 | 说明 |
---|---|---|---|
DEFAULT_INITIAL_CAPACITY | int | 16 | 默认初始容量,必须为 2 的幂 |
DEFAULT_LOAD_FACTOR | float | 0.75f | 默认加载因子 |
MAXIMUM_CAPACITY | int | 1 << 30 | 最大容量 |
TREEIFY_THRESHOLD | int | 8 | 桶中链表节点数量超过此值(且 table 长度足够)会转为红黑树 |
UNTREEIFY_THRESHOLD | int | 6 | 红黑树节点数量不足该值时,退化为链表 |
MIN_TREEIFY_CAPACITY | int | 64 | 树化所需的最小 table 容量(低于此值不树化) |
二 hash()
我们先从最最最最最最重要的hash(Object key)入手,源码如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
代码非常简短,但其非常之重要,我们通常将其称为扰动函数(hash spreading function)
众所周知,map的底层结构是数组 + 链表(+红黑树),因为hash值针对不同的key可能出现相同的hash,所以就必然会产生哈希冲突,扰动函数就是为了减轻哈希冲突出现的可能性的,我们直接分析代码。
其使用了一个三元运算符,左边的我们就不看了,直接看右边的一坨
(h = key.hashCode()) ^ (h >>> 16)
首先调用了对象的hashCode()函数,之后获取对应的哈希值,将其赋值给h: h = key.hashCode()
之后将其进行无符号右移动操作,将h向右边移动16wei位:h >>> 16
高位则用0补充,那么这个时候,由于经过hashCode()方法获取的hash值一定是32位的(int 的范围大小),所以其高16位一定是空的,那么此时高16位置跟低16位就能够进行一定的操作了,这里使用了与运算(当且仅当两个位都是1才是1,其余情况都是0),以实现高位参与哈希桶的索引计算,进一步的降低哈希冲突发生的概率。
这时有的同学就要问了,hashCode()到底是啥?简单来说,hashCode()就是提供对象的“身份哈希码”
其实,hashCode方法就是用来获取某个对象的原始哈希值,这个方法是从Object类当中继承下来的
@IntrinsicCandidate
public native int hashCode();
在不对其进行重写的情况下,其是一个native方法,底层是通过JVM实现的,将对应对象的地址通过一定的方式转换为某种变值,所以说这种方式下,对象的hash值跟对象的实际地址的高低是有关的。通过对其重写,来适应于某些特定场景。
再总结一下,通过使用hash函数而不是直接调用hashCode,hashMap可以降低哈希冲突产生的概率,从而提高效率。
三 get()
get()方法的源码如下:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(key)) == null ? null : e.value;
}
首先设置局部变量节点e,之后调用getNode()方法,获取对应节点并且将其赋值给e,使用三元运算符进行空值判断
return (e = getNode(key)) == null ? null : e.value;
进入到核心方法,getNode(Object key)当中去看,源码如下:
final Node<K,V> getNode(Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & (hash = hash(key))]) != null) {
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;
}
逐层刨析,我们一步一步看:
Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & (hash = hash(key))]) != null)
......
}
return null;
}
首先,定义了几个局部变量,其中:
tab记录map中的节点数组table,first记录第一个节点 ,e用来记录当前节点,n记录当前map的table的长度,hash用来记录节点的哈希值,k用来记录当前Key;
之后,看第一个if当中的条件判断,先将table赋值给tab: tab = table 同时判断其是否为空。
再将当前tab的长度记录到n当中:n = tab.length 要求哈希表的长度大于0
调用hash函数,获取当前key的hash值,并且将其赋值给hash:hash = hash(key) 同时要求其不为null
OK,准备工作都做完了,我们已经能够保证对应key的有效性,同时获取到了对应的hash值等。
再进一步看
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);
}
第一个if很简单,就是看当前第一个元素是不是我们要找的对象,先比较hash值,再通过比较对象的地址或者是参数值来看是否一致,如果都满足,直接返还第一个元素:
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
这个代码主要针对首次就找到对应key的情况
第二个if当中,先将first的下一个节点的值赋给e,同时确保不为空:(e = first.next) != null
之后先判断当前桶下的节点类型是不是 树 这是针对 JAVA1.8版本更新的,如果是树类型的,那么则采用getTreeNode()的方式获取对应节点:
if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key);
在1.8版本之前,hashMap的底层数据结构是:数组 + 链表,为什么又新增了红黑树?什么情况下用红黑树?我们等到讲到put再细说。
进入到do while循环当中,这个就简单了,其实就是将当前桶中的元素,按照链表从前到后一个一个遍历,如果找到了,那么直接返还。
四 put()
源码如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
内部调用了putVal函数,其中包含五个参数, 来看一下官方解释:
* @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.
前三个就不解释了,看后两个
onlyIfAbsent:如果是true,那么就不修改已经存在的值,也就是说,如果你两次放置了同样的KEY和VALUE,那么当前参数如果为true,就不会基于原本的key去修改了;反之则修改。hashMap当中默认为false,基于原本KEY修改
evict:如果为false,表明当前数组table正处于创建模式中。这里的创建模式代指两种情况:
其一,当前table还没有被分配,HashMap还没有分配table
其二,当前的table正处于扩容中
hashMap中默认为true,即数组没有处于创建模式,这时候可以正常进行插入等操作。
来吧,参数了解完了,我们来看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;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
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 {
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;
}
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;
}
令人头大的代码。。。。
我们一步一步看,拆分一下:
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
首先声明一些局部变量,之后进入到第一个if,同时将map的数组table赋值给tab,table的长度给n。如果为空,或者是对应的table表长度为0,就调用resize()方法,即重新初始化tab表,对其设置长度,一般是对tab进行首次初始化操作,这里涉及到一个关键方法 resize() 我们等下面细说
第二个if,将对应桶赋值给p,如果其为空:(p = tab[i = (n - 1) & hash]) == null
那么表明当前桶还是空的,我们直接添加即可:tab[i] = newNode(hash, key, value, null);
进入到else当中,put的核心代码区域,我们来看看到底写了啥:
else {
Node<K,V> e; K k;
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 {
.......
}
......
}
第一个if,判断当前桶的第一个节点p的hash值是否跟需求的一样,同时比较对应key是否一致,如果是,那么将当前节点p赋值给e,这种情况适用于桶中的第一个节点的key跟对象key一致:
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
else if,如果当前桶下节点的类型是TreeNode,则表明其已经被树化了,将对应的节点放在红黑树中,调用puyTreeVal(...)方法:
else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
如果以上两个条件都不满足的情况下 ,我们就要往后找了,再进入到else当中:
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;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
这个循环代码其实比较简单,binCount用来记录当前桶遍历过程中所处的位置以及链表长度,
第一个if,将当前节点p的下一个节点赋值给e,如果为空,表明当前链表已经遍历结束了,直到遍历结束也没有发现对应符合要求的hash,也就是表明并不存在对应hash值,我们直接创建新的节点,并且将其赋值给p:p.next = newNode(hash, key, value, null);
树化
其内部还有一个if,我们看到常数 TREEIFY_THRESHOLD 一眼就能看明白,奥!这是对应将链表树化的长度!如果当前链表的长度binCount超过了树化的长度,这个时候,内部就会调用treeifyBin,从而直接将对应的链表转化为红黑树,只要链表长度超过8就转化为树,真的是这样吗?这对吗?先放一放,我们先来看看这个常数是多少:
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
叽里咕噜说了一堆,简单说就是若当前数组table的存储长度大于等于8,那么就将list转化为tree,即树化
别急,通过源码我们还能看到,还有几个跟 Tree 有关的常数:
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
UNTREEIFY_THRESHOLD : 也就是反树化的常数,当当前Tree的长度小于等于6的时候,就会将其重新转化为list集合。
思考:不对啊,树化的时候是8,反树化不应该是7吗?中间为什么要差一个?
其实这是特意这样设计的,因为有的时候,部分桶可能会反复的删除或者增加同一个KEY,这个时候,如果其刚好处于7 - 8之间,那么就一直在树化跟反树化之间蹦迪了,树化是非常耗时的!为了避免这种情况的发生,特意中间留了一个空间,避免一直“蹦迪”
MIN_TREEIFY_CAPACITY :最小的table长度,如果表没有超过这个长度,那么就不对list集合进行树化,即使对应桶下的list长度超过了TREEIFY_THRESHOLD ,也不树化!换言之,两者一定是联合使用的。
我们回收一下问题:如果当前链表的长度binCount超过了树化的长度,这个时候,内部就会调用treeifyBin,从而直接将对应的链表转化为红黑树,只要链表长度超过8就转化为树,真的是这样吗?这对吗?
树化条件解析
在这里我们能看到,并不是这样,在JDK1.8版本中,控制链表转化为红黑树是有两个限制条件的
1. 桶下链表长度超过 TREEIFY_THRESHOLD
2. table数组长度超过 MIN_TREEIFY_CAPACITY
这一点何以见得呢?记得上面说的吗,put方法中,如果当前链表的长度超过了树化的长度,就会进入到方法treefiyBin
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
如果满足了第一个要求之后,进入到treeifyBin源码当中:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
看!第一步if条件判断,就是要求对应的tab为空时,或者对应桶的链表长度小于MIN_TREEIFY_CAPACITY的时候,对其重新进行resize() 扩容操作,也就是说,只有大于了MIN_TREEIFY_CAPACITY,才能执行树化的操作!也就满足了以上两点树化的要求
扯的有点远了,回归正题,我把代码放下来,大家不用往上面翻了:
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;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
进入到第二个if,如果当前的hash值跟我们需求的一样,并且对应key的值也一致(各位大佬到这里应该都能轻易看懂了),就说明我们找到了一个符合要求的链表中间的值,直接结束:
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
确定了对应key的位置之后,下一步就该放入对应的值了,代码如下:
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
如果对应的e不为空,也就是说我们是找到了跟我们的key匹配的位置,如果是一直没找到,这里的e是空的,直接放在链表末端
之后再判断一下,如果对应的节点e的值为空,或者是 !onlyIfAbsent,这个是最开始put传输过来到putVal的参数,记得吗?默认为fasle,即允许覆盖旧的key的值,它的作用就体现到这里了
afterNodeAccess()
然后会执行一个 afterNodeAccess(e);方法,在HashMap当中,这是一个钩子函数(Hook),即hashMap内部并没有实现,而是在其子类LinkedHashMap当中实现了
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
这种钩子函数,其本质其实还是 多态
当实现类为子类LinkedHashMap的时候,会执行子类的方法,例如:
HashMap<Object, Object> linkedHashMap = new LinkedHashMap<>();
这是JAVA多态的典型体现,即编译时看父类,运行时看子类
还差最后一步,put方法就结束了:
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
fail-fast机制
首先对成员变量modCount做了自增,利用fail-fast机制用于迭代时检测并发修改,我们简单说下fail-fast机制:
在 HashMap
中,fail-fast机制(快速失败)是一种用于防止在遍历过程中发生结构性修改(如添加或删除元素)导致不一致行为的机制。它主要通过一个字段 modCount
来实现。
即当你在使用迭代器(如 Iterator
)遍历 HashMap
时,如果在遍历过程中修改了结构(put/remove),就会触发 ConcurrentModificationException
,这就是 fail-fast。
-
每次结构性修改(如
put
、remove
)都会让modCount++
。 -
迭代器初始化时会保存
expectedModCount = map.modCount
。 -
每次迭代时对比
modCount
和expectedModCount
。-
通过比较,如果不一致,就说明结构被修改了 → 抛出异常。
-
之后看当前的长度是否大于等于扩容容量,大于则进行扩容,之后依旧是调用钩子函数:afterNodeInsertion,依旧是子类LinkedHashMap的,主要是用于对插入节点之后,再做一些事情,这里我们就不详细说明了。
还有一个扩容机制,我们等下一篇详细说,希望大家能耐心看完,并且有所收获 ^ - ^