HashMap底层源码分析
- 一.HashMap基础
- 二.何时触发扩容
- 三.扩容机制
- java1.7下扩容机制
- 元素迁移
- java1.8+扩容机制
- 元素迁移
一.HashMap基础
HashMap继承了AbstractMap抽象类,实现了Map,Cloneable,Serializable接口。
HashMap的源码属性:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
//初始容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子默认值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//桶上的链表长度大于等于8时,链表转化成红黑树
static final int TREEIFY_THRESHOLD = 8;
//桶上的红黑树大小小于等于6时,红黑树转化成链表
static final int UNTREEIFY_THRESHOLD = 6;
//当数组容量大于64时,链表才会转化成红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
- 初始容量:默认为16,在第一次put时生成
- 加载因子:默认为0.75
- 阈值:阈值=容量*加载因子。默认12,当元素数量超过阈值时便会触发扩容
- 最大容量:230 ,当容量超过230 ,容量不再扩大,阈值变为231 -1
- java1.8+,桶上的链表长度大于等于8时,链表转化成红黑树
- java1.8+,桶上的红黑树大小小于等于6时,红黑树转化成链表
- java1.8+,只有当数组容量大于64时,链表才会转化成红黑树
二.何时触发扩容
一般情况下,当元素数量超过阈值时便会触发扩容。每次扩容的容量都是之前容量的2倍。
注意:
HashMap的容量必须为2的n次幂,进行扩容时其容量变为不小于指定容量的2的幂数(即初始化容量过程)。
例如:进行有参初始化时:
- new HashMap<>(5)–>初次put时,容量扩为23 =8,数组长度达到阈值6,扩容为16
- new HashMap<>(7)–>初次put时,容量扩为23 =8,数组长度达到阈值6,扩容为16
- new HashMap<>(13)–>初次put时,容量扩为24 =16,数组长度达到阈值12,扩容为32
- new HashMap<>(19)–>初次put时,容量扩为25 =32,数组长度达到阈值24,扩容为64
三.扩容机制
当HashMap决定扩容时,会调用HashMap类中的resize()方法进行扩容。
java1.7下扩容机制
HashMap的底层结构在java1.7版本之前是:数组+单链表。
resize()源码:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {//当原有table长度已经达到了上限,不再扩容。
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
源码可见,当原table容量没有到达最大,则会新建一个新的数组,并调用transfer()方法进行元素迁移(见下)。
- 第一次put时,容量扩容初始化:其容量变为不小于指定容量的2的幂数(默认为16)
- 不是第一次put时,扩容:新容量=旧容量 * 2 ,新阈值=新容量 * 加载因子
元素迁移
transfer()源码:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
在准备好新的数组后,map会遍历数组的每个“桶”,然后遍历桶中的每个Entity,重新计算其hash值(也有可能不计算),找到新数组中的对应位置,以头插法插入新的链表。
注意:
- 因为是头插法,因此新旧链表的元素位置会发生转置现象。
- 元素迁移的过程中在多线程情境下有可能会触发死循环(无限进行链表反转),因此HashMap线程不安全。
java1.8+扩容机制
HashMap的底层结构在java1.8版本之后是:数组+单链表/红黑树。
resize()源码:
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;
}
java1.8与java1.7扩容时容量的计算方法都为扩大为原来容量的二倍。
注意:
因为1.8版本之后HashMap的底层结构为:数组+单链表/红黑树。因此如果某个桶中的链表长度大于等于8了,则会判断当前的hashmap的容量是否大于64,如果小于64,则会进行扩容;如果大于64,则将链表转为红黑树。
元素迁移
java1.8+在扩容时,不需要重新计算元素的hash进行元素迁移。
而是用原先位置key的hash值与旧数组的长度(oldCap)进行"与"操作。
- 如果结果是0,那么当前元素的桶位置不变。
- 如果结果为1,那么桶的位置就是原位置+原数组 长度
值得注意的是:为了防止java1.7之前元素迁移头插法在多线程是会造成死循环,java1.8+后使用尾插法
注意:
java1.8 扩容的时候会判断当前的桶的位置有没有链表或者红黑树,如果没有链表或者红黑树,那么当前元素还是和JDK1.7中的求法一样,求新的桶的位置。如果有链表,那么链表的元素会按照上述方法求新的桶的位置。如果是红黑树,则会调用split()方法,将红黑树切分为两个链表,之后进行扩容操作