hashMap的扩容原理源码分析
1.hashMap为什么要扩容?
1) 根本原因:hashMap底层结构有数组,因为数组一旦创建,其长度不会发生改变.
例如:
创建长度为3的数组
int[] i=new int[3]; i[0]=1; i[1]=2; i[2]=3;
抛异常:ArrayIndexOutOfBoundsException i[3]=4;
因此,当我们不断的往hashMap集合中存储元素时,hashMap将会进行自动扩容,这也就是需要我们去了解hashMap的扩容原理
2.hashMap的概述
1)hashMap是以k-v形式存储的集合.
2)数据结构:数组+单向链表+红黑树;hashMap定义了Node<K,V>[]及两个静态内部类,分别为Node<K,V>,TreeNode<K,V>
数组
transient Node<K,V>[] table;
链表
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; 根据key计算出hash值
final K key;
V value;
Node<K,V> next; 指向下一个节点
}
红黑树
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父节点
TreeNode<K,V> left; // 左节点
TreeNode<K,V> right; // 右节点
TreeNode<K,V> prev; // 需要在下一次删除时断开链接
boolean red; //判断其节点为红色/黑色
}
由上述可知,一:Node<K,V>[]数组属性,其数组下标是通过当前数组长度-1和hash值进行与操作得出.其公式为:(table.length - 1) & hash]);二:Node<K,V>类为单向链表;三:TreeNode<K,V>类为红黑树,了解hashMap的数据结构后,我们才能清晰明了的分析其CURD及扩容的流程.
3.hashMap自动扩容原理
hashMap源码分析
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 默认初始容量 -->hashMap是懒加载模式,当第一次put元素时才为其容量初始化
static final int MAXIMUM_CAPACITY = 1 << 30; 最大容量 -->当hashMap集合的容量为最大时,不在进行扩容;
static final float DEFAULT_LOAD_FACTOR = 0.75f; 加载因子 -->hashMap进行扩容的参数
static final int TREEIFY_THRESHOLD = 8; 红黑树临界值 -->
static final int UNTREEIFY_THRESHOLD = 6; 链表临界值 -->当红黑树节点高度<=6时转为链表结构
static final int MIN_TREEIFY_CAPACITY = 64; 转为红黑树最小容量 -->当链表节点>=8且hashMap容量>=MIN_TREEIFY_CAPACITY时转为红黑树,避免链表节点过长导致其查询效率降低
transient Node<K,V>[] table; 数组属性
transient Set<Map.Entry<K,V>> entrySet; k-v对象
transient int size; 当前集合元素的个数
int threshold; 当前集合扩容阀值=当前集合容量*加载因子
final float loadFactor; 下次扩容加载因子
hashMap扩容原理
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; 当前数组对象赋值
int oldCap = (oldTab == null) ? 0 : oldTab.length; 通过判断当前数组是否为null得出当前集合的容量
int oldThr = threshold; 将当前集合的扩容阀值赋值
int newCap, newThr = 0; 声明新的容量和新的扩容阀值
if (oldCap > 0) { 判断当前集合容量是否>0
if (oldCap >= MAXIMUM_CAPACITY) { 判断当前集合容量是否>最大容量 为true则hashMap将不在扩容,返回当前集合容量.
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 判断:初始容量(16)=<当前集合容<=量最大容量(2^30),则进行扩容newThr = oldThr << 1 原基础的2倍
oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; }
else if (oldThr > 0) 默认为0,如果当前集合的扩容阀值>0,则表示当前集合非首次添加元素 例如当前hashMap中只存储一个元素,则oldThr=threshold=16*0.75=12 newCap = oldThr; 继续put一个元素时 没有达到扩容阀值,则当前集合的容量保持不变 else { 程序走到这里时,说明集合为首次添加元素,为其初始化默认的容量 newCap = DEFAULT_INITIAL_CAPACITY; 初始化容量 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } 下次扩容阀值 12
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); }
下次扩容阀值
threshold = newThr;
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) { 拿到数组首节点后置为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 { 当程序走到这里时,说明其节点为链表中的节点 首先将链表中的节点遍历出来
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) 为null时说明其为首节点,直接插入新哈希表中即可
loHead = e;
else
loTail.next = e; 当前节点为非首节点,继续判断到当前节点为尾节点时,通过尾插法直接插入到新哈希表中
loTail = e;
}
else {
if (hiTail == null) 程序走到这里时,说明当前节点在新哈希表中的数组下标与老哈希表不一致,相当于重新存储到新哈希表中,了解put方法,那么这里理解就很简单了
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
程序走到这里时,其for循环已遍历1次结束,重新判断lotail hitoail其是否为首节点,
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;返回新的哈希表.
}
总结:
由上述可知,如果hashMap是第一次添加元素,那么其容量为16,阀值为12,当集合元素个数>=阀值时,hashMap才会进行第二次扩容,hashMap集合第二次或第n次扩容时,会根据当前集合的容量进行判断,
初始容量=<当前容量<=最大容量,如果集合当前容量>=最大容量时,将不在进行扩容,当前集合容量<=最大容量且>=初始容量时,在原来的基础上向左偏移1,也就是说新容量为原容量的2倍.
其次,非首次扩容时,将老哈希表中链表和树的节点添加到新哈希表中的问题,通过两个循环,外循环遍历出所有的节点,内循环将老哈希表的节点添加到新哈希表中去.
最后,此随笔是本人对hashMap扩容原理的理解,可能存在许多不足的地方,请见谅!
建议:对于初学者来说,最好以向hashMap put一个元素的视觉进行打断点,分析源码的每个步骤,然后做笔记,彻底掌握hashMap后,最好手写一个简陋的hashMap且进行测试哈.这样不仅仅提高你的阅读源码的能力,还提高你的编程思维等等.