前言
这篇文章会对HashMap的源码进行解析,HashMap不光是我们日常开发会经常用到,在面试时也经常会被问到。希望大家看完这篇文章之后对于HashMap不光知其然,也能知其所以然。
一、put(K key, V value)
大家在用到HashMap时,都知道要put方法需要两个参数,一个key,一个value。那么具体是怎么进行存储的呢,接下来我们就先看看put方法的源码。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
首先我们不管putVal这个方法是做什么的,先看下hash(key)这个方法是什么意思,返回了什么值,先搞清所有的参数,再去看putVal。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以看出这个方法返回的是一个int值,(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)的意思,大家都是老司机了,应该也不陌生,如果传入的key是null,那么就返回0,否则返回(h = key.hashCode()) ^ (h >>> 16)。重点就是这里,可以称之为扰动函数。实际就是h和h >>> 16进行按位异或运算(两个数转为二进制,然后从高位开始比较,如果相同则为0,不相同则为1)。既然是右移16位,那么我们可以以16作为一个分水岭。如果本身大于16位(高16位中有1),那么右移16位之后,低16位全部消失,就变成了高16位全是0,低16位有1。换句话说就是高16位不变,低16位与高16位进行异或运算,增加低位的随机性。如果本身就小于等于16位,那么其实就没有影响,原样输出,不会触发扰动函数。
接下来回到putVal,这里我们先不赘述,我们先来了解一下resize()方法,扩容方法也是HashMap中最重要的方法。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, 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)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
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)
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 = e.next;
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);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
首先定义oldTab并将table赋值给它,这个table是本身定义的一个空的Node数组,所以在我们第一次put时,这个oldTab和table都是null。oldCap 就是旧的容量,如果oldTab是null,那么它就是0,否则就是现有数组的长度。oldThr 赋值阈值。接下来赋值新容量和新阈值为0。resize方法太长了,我们分解来看。
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
判断语句:
1.如果旧容量大于0,也就是说本身已经存在数据了。再次判断:
(1)如果老table数据长度大于等于MAXIMUM_CAPACITY(1<< 30),那么阈值就等于Integer.MAX_VALUE。直接返回老table。之前总提到阈值,其实阈值的意思就是,每一次扩容不是说要等到容量全部都满了才去扩容,而是到达一定数量就开始扩容,这个数量就是阈值 。上面也提到过阈值=容量*负载因子。eg.现在容量是16,负载因子是0.75,那么阈值就是12,也就是当数组长度达到12的时候就开始扩容了。
(2)newCap = oldCap << 1就是现在新容量是旧容量的2倍,DEFAULT_INITIAL_CAPACITY(1 << 4)=16。这样就能明白,如果新容量小于MAXIMUM_CAPACITY并且旧容量大于16,那么新的阈值赋值为旧阈值的2倍。
2.如果旧表没数据并且旧阈值大于0,新容量就等于旧阈值。
3.如果上面两种情况都不满足,即旧表没数据,并且没有初始化容量和阈值。那么新容量就为默认容量16,新阈值就是默认容量16乘以默认负载因子0.75等于12。
接下来判断newThr == 0,即对应的是当前表是空的,但是有阈值的情况。如果等于0就给newThr进行赋值,然后进行越界修复。
最后将新阈值赋值给threshold这个全局变量。
这部分代码其实就是根据情况,计算出新的容量和新的阈值。
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)
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 = e.next;
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);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
我们计算完新的容量和新的阈值,接下来这部分,其实想想都知道,肯定是要把旧table的内容放进新table里,我们来看看是怎么做的。
首先根据新容量定义一个新的Node数组。然后判断旧table是否为null,如果旧table有数据,那么就正式开始操作了。大家都知道HashMap是数组+链表结构,for循环中的j其实就是数组的index。如果oldTab[j]不是null,那么先将oldTab[j]指向null,通知回收。然后开始条件判断:
1.如果e.next == null,也就说e是链表中的最后一个节点,已经没有后置节点了。那么将e.hash 和(newCap - 1)进行按位与操作,计算出新的index,然后将e放在新表的新下标的链表中。
2.在这种情况下,e就是一个树节点。也就是转到红黑树结构了,篇幅有限这里就不细讲了,大家可以看下其他的视频或文章学习下。
3.看似很复杂,其实就是分成了两个部分,高位区和低位区,高位区和低位区可以理解为,低位区就是原来的index区间,高位区是扩容后增加的index区间,比如旧容量是16,扩容后变成32,那么现在低位区就是index0-15,高位区就是index16-31。Head和Tail对应的就是头尾指针。逻辑就是将原来每个下标中的链表进行循环,之后并不是赋值在原来的位置上了,而是通过判断,将一部分迁移到高位区。如果对代码中头尾指针的移动有困惑可以看我前一篇讲LinkedList和ArrayList源码的文章,能有所帮助。之后就是将高位区和低位区进行结合组成newTable,然后返回。
到这扩容方法结束,我们现在转回去看一下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;
}
定义好变量后,进行判断,(tab = table) == null || (n = tab.length) == 0,这个意思就是如果是一个全新的table,那么这个时候n是多少呢?上面扩容方法中讲过全新的table首次扩容,默认容量是16。所以这个时候n=16。
接下来是一个if else的判断:如果计算后的下标i在数组中没有数据,那么就新建一个节点,否则:
1.如果与已存在的Node是相同的key值,直接替换;
2.判断是否是树节点;
3.确定是链表的普通节点,循环遍历链式Node,并对比hash和key,如果都不相同,则将新的Node拼装到链表的末尾。如果相同,则进行更新。
接下来其实就是接着步骤1,满足条件就进行值替换。onlyIfAbsent 如果是true,那么就不改变已存在的value值。
接下来增加修改次数,如果现在的size大于阈值就进行扩容。
到这HashMap的put方法就结束了。可能来回跳的有点乱,建议大家重点看一下reSize方法,然后对照着源码整个过一遍,有问题的地方再看回到这里。
二、get(Object key)
get方法肯定传入一个key来获取value,那么这个value是怎么来的,我们接着来看源码。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
首先我们不管getNode方法具体做了什么,两个参数,一个是对key进行hash,另外一个就是key。返回的肯定是个Node,如果是个null,那么就直接返回null,否则就返回这个节点的value。接下来我们就看看getNode到底是怎么get的这个Node。
final Node<K,V> getNode(int hash, Object key) {
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) {
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;
}
首先就是判断table不是null并且tab[(n - 1) & hash]不是null,那么就再进行判断,否则直接返回null。
接下来判断的就是当前下表链表的第一个结点就符合条件,那么就直接把第一个节点返回。
如果first节点的下一个节点不是null,那么先判断是不是树节点,如果是树节点,就从红黑树取值,如果不是就循环遍历链表进行取值。
如果最后还是没有符合条件的就返回null。
总结
上面主要讲了HashMap的put和get的源码。接下来再补充一下。
1.转红黑树的条件不止是链表长度达到了转树的阈值(默认为8),在树化(treeifyBin)方法中还进行了判断,tab的长度要小于MIN_TREEIFY_CAPACITY(默认64)。当然红黑树还可以转回链表,条件是当链表的值小于UNTREEIFY_THRESHOLD(默认6)。为什么会是链表大于8就树化,这里转载一个链接给大家,可以参考为什么?侵删。
2.如果大家看过源码之后还是有点蒙,或者有点看的一知半解,这里建议大家debug,跟着流程走一下,可能会加深理解,并且建议key采用Integer类型,因为Integer类型hashCode的值就等于本身,方便后续自己的计算。
3.不管是put还是get都重点运用了hash和equals方法。这里的equals方法是重写过的因为equals方法本身只对引用进行比较。
因为我的了解可能也不是那么深刻,所以难免有错误或者没有提到的地方,有问题希望大家能在评论中指正,大家一起讨论,共同进步!