HashMap部分源码阅读
HashMap核心属性分析
//默认的初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量 2^30
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;
//树化要求的最小容量,在达到树化阈值后要求散列表的容量达到64才会树化
static final int MIN_TREEIFY_CAPACITY = 64;
/* ------------------------ Fields ------------------------ */
//散列表
transient Node<K,V>[] table;
//hashmap中存储的键值对的数量
transient int size;
//修改次数(修改映射结构和rehash等),修改值不算
transient int modCount;
//下一次进行扩容的阈值(等于容量 * 负载因子)
int threshold;
//负载因子
final float loadFactor;
结点结构分析
static class Node<K,V> implements Map.Entry<K,V> {
//key的hashcode经过扰乱函数后得到的hash值
final int hash;
//原始的key
final K key;
//值
V value;
//下一个结点
Node<K,V> next;
//……
}
HashMap构造方法分析
构造方法无非一个个套娃,所以看参数最多的那个构造方法就可以了。
public HashMap(int initialCapacity, float loadFactor) {
//对传进来的 initialCapacity 和 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);
}
所以要搞懂构造方法,就得看tableSizeFor(int cap)
这个方法。
/**
* 注意:对于给定的目标容量,返回两倍大小的幂,也就是说hashmap的容量一定是2的幂
* Returns a power of two size for the given target capacity.
*
* 假定给定的目标容量为 5
* n = cap - 1 = 4 = 0b0000 0000 0000 0100
* n |= n >>> 1 => 0b0000 0000 0000 0100 | 0b0000 0000 0000 0010 => 0b0000 0000 0000 0110
* n |= n >>> 2 => 0b0000 0000 0000 0110 | 0b0000 0000 0000 0001 => 0b0000 0000 0000 0111
* n |= n >>> 4 => 0b0000 0000 0000 0111 | 0b0000 0000 0000 0000 => 0b0000 0000 0000 0111
* ……
* 经过这些操作之后,n的值为 7 最后就会返回 8 (aka 2^3)
*
* 注意:在进行移位和或操作之前先将给定的目标容量 - 1 了,为什么要这么操作?
* 假设给定的目标容量为 16 且不进行 -1 操作
* n |= n >>> 1 => 0b0000 0000 0001 0000 | 0b0000 0000 0000 1000 => 0b0000 0000 0001 1000
* n |= n >>> 2 => 0b0000 0000 0001 1000 | 0b0000 0000 0000 0110 => 0b0000 0000 0001 1110
* n |= n >>> 4 => 0b0000 0000 0001 1110 | 0b0000 0000 0000 1111 => 0b0000 0000 0001 1111
* n |= n >>> 8 => 0b0000 0000 0001 1111 | 0b0000 0000 0000 0000 => 0b0000 0000 0001 1111
* ……
* 最后n的值为31 再经过 +1 就会返回 32 所以本来只要16的容量 最后返回值却翻了倍 所以一开始要进行 -1操作
*
*/
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;
//如果n < 0 则返回 1,否则判断 n 是否大于等于最大容量,如果大于等于最大容量则返回最大容量,否则返回 n + 1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
HashMap put 方法分析
先看put方法。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
put方法套娃了putVal()
,然后调用putVal()
时还用了hash(Object key)
,所以先看hash(Object key)
。
/**
*
* 对key的hashcode进行一个扰乱,让发生hash碰撞的概率降低
* 最后返回 key 是否等于 null 如果等于null 则返回0 否则返回 (h = key.hashCode()) ^ (h >>> 16)
*
* 假设:
* h = 0b0101 0010 1101 0010 0001 1101 0010 0100
* h >>> 16 = 0b0000 0000 0000 0000 0101 0010 1101 0010
* ^= 0b0000 0000 0000 0000 0001 0000 0000 0000
* 可以看出 h >>> 16 的目的是为了将高位的数值移到低位,让高位的数值也参与计算
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
看完 hash(), 我们回到 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是散列表 n是散列表的长度
if ((tab = table) == null || (n = tab.length) == 0)
//如果散列表还没初始化则进行初始化 并将初始化后的长度赋给n,初始化后的散列表赋给tab
n = (tab = resize()).length;
//(n - 1) & hash :寻址算法 通过这个算法找到键值对要存放的位置
//找到要存放的位置之后将 位置的值赋给i 并将散列表这个位置的结点赋给p
//如果p == null 即表示在散列表这个位置还未存放数据
if ((p = tab[i = (n - 1) & hash]) == null)
//将传进来的键值对生成结点放在散列表的这个位置
tab[i] = newNode(hash, key, value, null);
//散列表中这个位置已经存放了结点
else {
Node<K,V> e; K k;
//如果当前结点的hash值与传进来的hash值相等并且(key相同或key值相等)
//总的来说就是 要增加的结点和当前结点相等,所以直接替换
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;
}
//遇到了相等的key或相同的key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//直接退出 因为找到了
break;
p = e;
}
}
//e不为空就代表找到了与新结点的key一样的结点
if (e != null) { // existing mapping for key
V oldValue = e.value;
//onlyIfAbsent == false 或者 oldValue == null
//onlyIfAbsent的意思是:如果存在值了是否替换,false表示替换,true则不替换
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//在LinkedHashMap中实现,目的是将结点e放到最后去
afterNodeAccess(e);
//返回旧的数值
return oldValue;
}
}
//修改次数+1
//进行到这里,也就是说没有找到key值相同或相等的结点,也就是没有value值替换才会modCount++
++modCount;
//存放的结点数量+1后如果大于扩容阈值(容量 * 负载因子)
if (++size > threshold)
//进行扩容操作
resize();
//LinkedHashMap里实现,是移除最老的首节点,但原文中说了是可能移除,也就是说要看removeEldestEntry是怎么重写的
//但默认removeEldestEntry方法返回false,所以这个方法想要运行就要重写removeEldestEntry方法
/**
* 举例:
* 当超出缓存容器大小时移除最老的首节点:
* @Override
* public boolean removeEldestEntry(Map.Entry<K, V> eldest) {
* return size() > capacity;
* }
*/
afterNodeInsertion(evict);
return null;
}
看完putVal()
,我们发现还有三个地方需要进一步深入去分析:
((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
即树版本的插入操作treeifyBin(tab, hash);
树化操作resize();
扩容操作
先分析红黑树相关的代码,红黑树相关的操作与红黑树博客上的类似,所以只写了一丢丢帮助理解的注释。
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//父结点
TreeNode<K,V> parent; // red-black tree links
//左结点
TreeNode<K,V> left;
//右结点
TreeNode<K,V> right;
//前一个结点
TreeNode<K,V> prev; // needed to unlink next upon deletion
//颜色 默认是红色
boolean red;
//……
}
/**
* Tree version of putVal.
* 红黑树版本的插入值
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
//ph:当前结点的hash值
//pk:当前结点的key值
int dir, ph; K pk;
//要放入的结点的hash值小于根节点的hash值
if ((ph = p.hash) > h)
dir = -1;
//要放入的结点的hash值大于根节点的hash值
else if (ph < h)
dir = 1;
//要放入的结点的key值和当前结点相同或者key值相等
//直接返回当前结点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
//插入和平衡操作
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
//n:散列表tab的长度
//index:根据hash和寻址算法找到的要树化的桶的位置
//e:存放当前结点
int n, index; Node<K,V> e;
//如果散列表未初始化或者散列表的长度未达到树化的最小容量 = 64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//则进行扩容
resize();
//判断要树化的桶存放的头结点是否为null
else if ((e = tab[index = (n - 1) & hash]) != null) {
//hd:头结点
//tl:尾结点
TreeNode<K,V> hd = null, tl = null;
//通过循环将每个结点转换成树结点,在前后结点建立起prev和next的指针,并且找到头节点和尾结点
do {
//将当前结点e转换成树结点
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);
}
}
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
//将根结点的颜色变为黑色
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
//ph:结点p的hash值
//pk:结点p的key值
int dir, ph;
K pk = p.key;
//比较hash值,看是比p结点大还是小还是相等
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
//dir<=0即p结点的hash>=x结点的hash,就是p结点的左结点,否则则为右结点
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//每新插入一个结点都要进行平衡操作
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
//红黑树的平衡操作(变色、左旋、右旋)
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
x.red = true;
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
else if (!xp.red || (xpp = xp.parent) == null)
return root;
if (xp == (xppl = xpp.left)) {
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
else {
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
插入、平衡操作都在红黑树博客里。
接着分析 resize(),也就是扩容操作。
final Node<K,V>[] resize() {
//扩容前的散列表
Node<K,V>[] oldTab = table;
//oldCap:oldTab == null,也就是说还未初始化,则 = 0,否则等于 oldTab.length(扩容前的散列表的长度)
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//oldThr:扩容前的扩容阈值
int oldThr = threshold;
//newCap:扩容后的新的容量
//newThr:扩容后新的扩容阈值
int newCap, newThr = 0;
//散列表已经初始化了
if (oldCap > 0) {
//要进行扩容的散列表的容量已经 >= 最大容量了(2^30)
if (oldCap >= MAXIMUM_CAPACITY) {
//将扩容阈值设置成整型的最大值(2^31 - 1),也就是说再也不会扩容了
threshold = Integer.MAX_VALUE;
return oldTab;
}
//扩容后的新容量(扩容前的容量 << 1,也就是乘以2)< 最大容量(2^30),并且扩容前的容量 >= 默认的初始容量(16)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//新的扩容阈值也等于旧的扩容阈值乘以2
newThr = oldThr << 1; // double threshold
}
//散列表未初始化且旧的扩容阈值 > 0
else if (oldThr > 0) // initial capacity was placed in threshold
//初始化容量被旧的扩容阈值替代
newCap = oldThr;
//散列表未初始化且旧的扩容阈值 = 0
else { // zero initial threshold signifies using defaults 零初始阈值表示使用默认值
//新容量等于默认初始容量
newCap = DEFAULT_INITIAL_CAPACITY;
//新扩容阈值等于默认的初始阈值 * 负载因子
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//新的扩容阈值 = 0,也就是上面散列表未初始化且旧的扩容阈值 > 0 的情况,在这个情况下 newThr未被赋值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
//如果新容量 < 最大容量 且 ft,即上一行算出来的新的扩容阈值 < 最大容量,则新的扩容阈值等于上面算出来的
//否则等于Integer.MAX_VALUE
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,接下来的操作就是将旧的散列表的值转移到新的散列表了
table = newTab;
//旧的散列表是已初始化了的
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//oldTab[j]这个桶里只存放了一个数据
if (e.next == null)
//通过寻址算法重新寻址并将这个数据存放进去
newTab[e.hash & (newCap - 1)] = e;
//oldTab[j]这个桶里放了一棵红黑树
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
/**
* Q:为什么要分高位、低位?为什么高位所在的桶位是低位所在桶位 + 旧的容量?
* A:因为长度增加了,一个桶位里的数据可能因为高一位的值不同而产生变化,而变化量就为旧的容量。
* 举例:
* 假设原来的容量是16,扩容后的容量就为32
* 扩容前:
* e1.hash = 0b0000 1011 & 0b0000 1111 => 0b0000 1011 = 11
* e2.hash = 0b0001 1011 & 0b0000 1111 => 0b0000 1011 = 11
* 扩容后:
* e1.hash = 0b0000 1011 & 0b0001 1111 => 0b0000 1011 = 11
* e2.hash = 0b0001 1011 & 0b0001 1111 => 0b0001 1011 = 27
*/
//放了一个链表
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;
//e.hash & oldCap == 0,也就是说e.hash高一位也是0
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//e.hash高一位是1
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
//高一位是0的,桶位不变
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
//高一位是1的,桶位加上旧容量
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
HashMap的put方法就分析到这啦,其他的相信你们自己也能自行分析了。
如有错误,欢迎指正!