**
HashMap源码解析
**
总结:
- HashMap在JDK1.8之前 是由数组+链表组成,在JDK1.8之后是由数组+链表+红黑树组成,在一般是通过key
进行hash算法得到一个hash值,通过hash值去对hash桶进行数据插入,并且会累加当前元素的长度,当然也存在不同的key计算出相同的hash值,这个就是我们常常听到的hash冲突。
2.在发生hash冲突的时候,会先比对他们的hash值和他们的key,如果相同则会记录当前操作结束方法,如果不相同则会循环判断当前链表的元素的节点是否大于等于8,如果大于等于8则会执行树化方法将链表转化成红黑树,否则会在当前元素的末尾追加一个元素
,3.本文主要是将JDK1.8之后的HashMap。 首先HashMap在没有put操作之前他的初始容量是0,只有在put操作的时候才会进行Hash桶的扩容,HashMap 第一次扩容数组长度是16,每次扩容是之前的两倍。 HashMap在初始化的时候,一般只会给他的加载因子赋值,默认加载因子为0.75
重要属性:
//默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表转向红黑树的阈值,数组节点大于等于8时
static final int TREEIFY_THRESHOLD = 8;
//红黑树退化到链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//树化的条件2,必须数组节点大于等于64时
static final int MIN_TREEIFY_CAPACITY = 64;
常用构造方法
//常用的hashmap构造函数,这里只做了一个操作,就是给加载因子赋值 0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
put方法的分析
//传入参数key,value,返回value
//调用putVal之前先对key进行了hash操作
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果table表等于null或者tab的长度等于0则调resize()方法进行扩容
//同时这里会将table赋值给tab,如果第一个条件不成立则会将tab的长度赋值给 n
if ((tab = table) == null || (n = tab.length) == 0)
//将扩容后的数组长度赋值给n
n = (tab = resize()).length;
//因为经过上面的赋值操作,这里用tab的数组长度 -1 取余hash 等到tabl[i]的元素赋值给p
//如果p等于null,就相当于当前数组下标位置没有元素,则创建一个新的节点,当前节点包含属性有(hash, key, value, null)并赋值到下标等于i的tab表下
if ((p = tab[i = (n - 1) & hash]) == null)
//创建新的节点将当前k,v,hash初始化进去,并赋值到tab下标为i下
tab[i] = newNode(hash, key, value, null);
else {
//如果以上方法逻辑都不成立则进入下面逻辑
//p!=null,说明计算的hash发生了重复,也就是hash冲突
Node<K,V> e; K k;
//如果当前当前节点p计算的hash和传入的hash相同,拿到的k等于传入的key,或者k等于传入的k,则直接替换
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//这里替换元素
e = p;
//如果查到的p元素是红黑树的节点
else if (p instanceof TreeNode)
//则按照红黑树的处理逻辑进行添加
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果都不是则进入死循环,
for (int binCount = 0; ; ++binCount) {//死循环
//如果p的下一个节点等于null
if ((e = p.next) == null) {
//则将新的参数添加到p节点下面
p.next = newNode(hash, key, value, null);
//如果当前链表长度大于等于8,则会执行树化逻辑
if (binCount >= TREEIFY_THRESHOLD - 1)
//执行树化逻辑
treeifyBin(tab, hash);
break;
}
//如果链表中有hash值和要传入的hash值形同,并且key也相同,则停止循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//将e赋值给p继续下一次循环
p = e;
}
}
//e 不等于 null
if (e != null) { // existing mapping for key
//将e的value值取出赋值给oldValue
V oldValue = e.value;
//onlyIfAbsent 传入时为false 取反为true 直接进入if
if (!onlyIfAbsent || oldValue == null)
//进行value覆盖操作
e.value = value;
afterNodeAccess(e);
//返回原来的value值
return oldValue;
}
}
//记录操作次数
++modCount;
//判读是否需要扩容,当前实际容量 +1 如果大于扩容阈值12 则进行扩容操作
if (++size > threshold)
//进行扩容操作
resize();
afterNodeInsertion(evict);
return null;
}
*重点 hashmap扩容方法 解析
> //初始化或加倍表的大小。 如果为空,按照字段阈值中持有的初始容量目标分配。
> 否则,因为我们使用的是2的幂展开,所以每个bin中的元素要么保持在相同的索引位置,要么在新表中以2的幂移动 final
> Node<K,V>[] resize() {
> //将当前table数组赋值给oldTab
> Node<K,V>[] oldTab = table;
> //当前数组长度,如果oldTab=null 则返回0否则返回当前长度
> int oldCap = (oldTab == null) ? 0 : oldTab.length;
> //如果数组没有被分配,threshold这个字段为数组初始容量或者0赋值给oldThr
> int oldThr = threshold;
> //创建新的数组扩容长度和新的扩容阈值
> int newCap, newThr = 0;
> //如果当前数组长度大于0,则执行下面逻辑
> if (oldCap > 0) {
> //当前数组长度大于等于最大值1<<30位
> if (oldCap >= MAXIMUM_CAPACITY) {
> //则扩容阈值等于整数的最大值
> threshold = Integer.MAX_VALUE;
> //返回当前数组
> return oldTab;
> }
> //(如果当前数组长度左位移一位 并赋值给新的长度 小于 1<<30)并且, //老的数组长度 >= 16 则 当前扩容阈值扩容右位移两位 也就是 *2 赋值给新的扩容阈值
> else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
> oldCap >= DEFAULT_INITIAL_CAPACITY)
> newThr = oldThr << 1; // double threshold
> }
> //如果扩容阈值大于0 则初始化长度为扩容阈值
> else if (oldThr > 0) // initial capacity was placed in threshold
> //则将原来的扩容阈值赋值给新的数组长度
> newCap = oldThr;
> else { // 初始阈值为零表示使用默认值 ,初始化扩容长度和阈值
> newCap = DEFAULT_INITIAL_CAPACITY;
> newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
> }
> //如果新的扩容阈值等于0 ,经过比较之后将计算得到的扩容阈值赋值给newThr 12
> if (newThr == 0) {
> float ft = (float)newCap * loadFactor;
> newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
> (int)ft : Integer.MAX_VALUE);
> }
> //新将扩容阈值给到threshold
> threshold = newThr;
> @SuppressWarnings({"rawtypes","unchecked"})
> //创建node数组,数组长度等于 新的数组长度newCap
> Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
> table = newTab;//将新数组赋值给table
> //判断如果老的数组不等于null则 进行扩容以及数组元素重新排序操作
> if (oldTab != null) {
> //循环每一个数组元素也就是位桶
> for (int j = 0; j < oldCap; ++j) {
> Node<K,V> e;
> //将下标为j的数组元素赋值给 e 且不等于 null
> if ((e = oldTab[j]) != null) {
> oldTab[j] = null;//将老数组置为null
> if (e.next == null)
> //如果e的下一个元素等于null则在新的数组中进行hash运算得到新的数组元素下标并将e 赋值过来
> newTab[e.hash & (newCap - 1)] = e;
> //如果 e 是树的节点,则按照树化的方式去转移数组元素
> else if (e instanceof TreeNode)
> ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
> //如果是链表
> else { // preserve order
> //分别创建rehash之后==0和=1的链表元素
> Node<K,V> loHead = null, loTail = null;
> Node<K,V> hiHead = null, hiTail = null;
> Node<K,V> next;
> do {//拿到当前节点的下一个节点
> next = e.next;
> //进行rehash,减少比较时的时间损耗,通过当前的hash值取余老的数组长度,因为当前hash值是经过hash计算之后的,所以取余oldCap必定是0或者1,这是将他们分成两个链表,将元素进行链表插入
> 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);
> //判断为节点不等于空,则将尾节点末尾置为null
> if (loTail != null) {
> loTail.next = null;
> newTab[j] = loHead;
> }
> //判断取余等于1的元素,将他的尾节点置为null,并建将头节点插入到当前下表+老数组长度的 数组位置
> if (hiTail != null) {
> hiTail.next = null;
> newTab[j + oldCap] = hiHead;
> }
> }
> }
> }
> }
> return newTab;
> }
附上put方法执行流程图 方便理解 流程图跟随箭头运行
hashmap扩容流程图
位桶数据转移流程图
学习地址