前言
HashMap作为一种键值对(KEY-VALUE)的数据结构,其设计思想,在日常开发中有着举足轻重的地位。作为Java开发的进阶学习,除了日常的使用之外,也应该对底层设计原理及源码有一些研究,便于开发过程中更好地选择数据结构、平衡性能、评估并发风险等。
存储结构
JDK1.8之后,为了提高Hash冲突的查询效率、解决循环依赖等问题,HashMap底层做了一些优化,其内部存储结构使用的是数组+链表或红黑树,如下图所示。
绿色节点的是KEY的HashCode值在数组中的位置(经过取模运算)。
红色节点有两种类型,针对链表结构,是实现Map.Entry<K,V>接口的Node节点;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
而对于红黑树结构,则是实现了LinkedHashMap.Entry<K,V>接口的TreeNode节点。
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;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
源码解析
内部属性
1、静态常量
属性名 | 属性值 | 备注 |
---|---|---|
DEFAULT_INITIAL_CAPACITY | 16 | 默认容量大小 |
MAXIMUM_CAPACITY | 1<<30 | 最大容量大小 |
DEFAULT_LOAD_FACTOR | 0.75 | 默认负载因子,负载因子*容量,作为扩容的临界条件 |
TREEIFY_THRESHOLD | 8 | 节点树化临界值,当节点数达到最小树化容量,且数组中节点个数超过此值,会进行树化 |
UNTREEIFY_THRESHOLD | 6 | 树化的节点数低于此值,会转成链表 |
MIN_TREEIFY_CAPACITY | 64 | 最小树化容量,当size低于此值,及时节点数不低于TREEIFY_THRESHOLD,也不会进行树化操作 |
2、私有属性
属性名 | 类型 | 备注 |
---|---|---|
table | Node<K,V>[] | 节点数组,HashMap的底层存储结构 |
entrySet | Set<Map.Entry<K,V>> | 遍历辅助set集 |
size | int | 当前Map中有效键值对数量 |
modCount | int | 当前Map修改次数,用于iterator,迭代中间modCount编发,会抛出ConcurrntModificationException异常 |
threshold | int | 扩容的阈值 |
loadFactor | float | 负载因子 |
重点方法剖析
0、HASH(KEY)
哈希函数比较简单,获取key的hashCode,并与其高16位取异或运算。如此设计的目的,是为了增加hash值的复杂度,降低hash冲突的概率。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1、CONSTRUCTOR
常用构造函数,做了参数的临界值判断之外,仅对loadFactor和threshold赋值,并未对table分配空间,这里使用了懒加载的思想,在put时分配内存空间,避免空间浪费。
public HashMap(int initialCapacity, float 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是HaspMap的内部静态函数,主要作用是获取进来的参数转变为2的n次方的数值。
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;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
如:
cap = 10; // 00000000 00000000 00000000 00001010
n = 9; // n = 00000000 00000000 00000000 00000101
n |= n >>> 1; // n = 00000000 00000000 00000000 00000111
后面右移取或,最终tableSizeFor的大小为16
2、GET(KEY)
3、PUT(KEY, VALUE)
这里可以看出,put函数的流程中,有两处显示的扩容调用。但在树化的过程中,如果当前size小于MIN_TREEIFY_CAPACITY,此时并不会进行树化操作,而是进行一次隐式扩容。具体用途如下:
1. 第一次扩容,是懒加载的设计思想,避免内存浪费
2. 第二次扩容,针对新插入的键值对,size自增后超过扩容阈值,发起的扩容操作
3. 第三次扩容(树化),当size小于MIN_TREEIFY_CAPACITY,此时会扩容来避免一次树化
4、RESIZE()
如前所属,HashMap扩容的几个时间点,容量是按照2倍去递增,基本流程如下:
- 扩容临界条件判断,是否支持扩容。计算新的扩容阈值、分配扩容后数组等
- 旧数组的节点数据迁移
数据迁移有几种情况(oldCap: 旧数组容量,newCap:新数组容量):
- i 位置的单个节点的迁移,位置变为【e.hash & (newCap - 1)】,保持不动,要么顺移至 (i + oldCap) 位置
- j 位置的链表迁移,按照【(e.hash & oldCap) == 0】条件拆分列表,条件为真,保持 j 位置不动,否则,移动至 (j + oldCap),保证链表节点之间的顺序性
- k 位置的红黑树迁移,类似链表迁移流程,先分割,再根据条件迁移
核心迁移代码实例如下:
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null; // 手动赋值为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;
}
}
}
}
这里手动将oldTab[j]赋值为null,借助jvm的垃圾回收机制,自动回收旧数组占用的内存
5、REMOVE(KEY)
remove函数,需要判断当前的KEY是否在集合中,因此会有get函数流程。获取到目标节点后,根据节点类型,删除节点,并返回删除节点的VALUE值
这里需要注意,如果删除的是TreeNode节点,红黑树平衡会有自旋平衡,同时可能会退化成链表结构