一:版本变更
HashMap在1.8之前采用数组 + 链表组合作为底层数据结构。1.8中针对解决Hash冲突的链表优化加入了红黑树的转换,即在链表长度达到限定阈值后将转换为红黑树结构。链长过长场景下遍历性能相对于链表会得到提升。本文将从元素插入、扩容两个方面尝试分析HashMap
二:重要属性
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// 无参构造、默认数组初始化大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 默认节点元素最大容量值
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;
// 链表转红黑树结构时桶中最小节点数量
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 存放节点总数
transient int size;
// 每次扩容和更改map结构计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时进行扩容
int threshold;
// 填充因子
final float loadFactor;
}
三:链表节点
链表节点由HashMap维护的内部类Node实现,该内部类定义四个属性
- hash:key值经过hash运算后产生的值,key相等与hash相等为充分不必要条件
- key:插入节点的key值
- value:插入节点的value值
- next:通过next域维护形成单向链表结构
四:红黑树节点
红黑树节点与链表节点一致,HashMap维护内部类TreeNode实现,具备以下五个属性
- parent:记录父节点
- left:左节点
- right:右节点
- prev:预计下一节点
- red:红黑节点标志
五:元素添加
5.1 流程示意图
具备以上关键属性、内部类知识后基本查看插入代码没有任何压力,都是比较简单的操作。个人感觉这个源码写得很恶心,逻辑不是很专一清晰,需要细心慢慢整理即可
5.2 重要节点提示
以上两个地方会涉及到扩容操作,第一点是当插入时发现Node[] table数组未初始化,第二点是当插入元素后判断节点数量大于阈值。透过第一点情况其实就很清晰位桶数组采用延时加载策略,即使用再加载
很多地方都会出现这个判断,前面说过,hash值相等key不一定相等。但key相等则其hash值一定相等
链表转红黑树操作在遍历链表且非覆盖原节点value时发生,仔细看前面的流程图就会发现插入时判断的层次为
位桶存在节点
位桶节点类型为树结构
循环链表判断key覆盖
六:扩容
扩容操作其实可以分为两部分阅读,第一部分是通过算法计算得出扩容后新数组大小及阈值,第二部分则是当扩容前存在节点则重新经过hash计算后加入新结构中
6.1 容量及阈值计算
首先根据代码可以知道在扩容resize()中夹杂了许多逻辑,可以说每一个分支都是在进行不同的操作
- 第一个分支oldCap即原容量大于0,若超过容量阈值则不扩容,若原容量大于16且扩容两倍后小于最大容量则扩容两倍。这个算法属于比较正常主干的分支
- 第二个分支针对初始化构造传入初始化容量的情况,当传入初始化容量小于16扩容时就会走这个分支,将计算的阈值用于新数组大小
- 第三个分支一看就是无参构造的延迟加载,默认数组大小16,阈值为16 * 0.75
6.2 元素转移
关于元素转移在JDK1.8我就提出一点,节点复制后的要么在原位置亦或是两倍的位置。如果两个节点原位置在一个桶,移动后还在一个桶,这两个节点的顺序不会发生改变。这个相对于JDK1.7的改变也解决了死循环的情况,细节请自行查看