HashMap源码解析
1 Hash的来由
1.1 数组的优劣势
-
优势
- 检索速度快
- 按索引遍历方便
- 能存储大量数据
-
劣势
- 占用空间多,浪费
- 数组大小固定,一经确定不能改变
- 只能存储一种类型的数据
- 增加删除效率低
1.2 链表的优劣势
- 优势
- 增删元素效率高
- 内存利用率高,不浪费内存
- 大小不固定,拓展灵活
- 劣势
- 检索需要遍历节点,效率低
1.3 整合两种数据结构的优势
-
散列表
-
散列表,又叫哈希表(Hash Table),是能够通过给定的关键字的值直接访问到具体对应的值的一个数据结构。也就是说,把关键字映射到一个表中的位置来直接访问记录,以加快访问速度。一种散列存储结构,通过关键字快速定位。
通常,我们把这个关键字称为Key,把对应的记录称为 Value,所以也可以说是通过 Key 访问一个映射表来得到 Value 的地址。而这个映射表,也叫作散列函数或者哈希函数,存放记录的数组叫作散列表。
其中有个特殊情况,就是通过不同的 Key,可能访问到同一个地址,这种现象叫作碰撞(Collision)。而通过某个 Key 一定会得到唯一的 Value 地址。
1.4 散列表的特点
- 访问速度快
- 需要额外的空间 (空间换时间)
- 无序
- 可能产生哈希碰撞
1.5 哈希的定义
核心理论:Hash也称散列、哈希,对应的英文都是Hash。基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出,这个映射的规则就是对应的Hash算法,而原始数据映射后的二进制串就是哈希值。
Hash的特点:
- 从Hash值不可以反向推到出原始的数据
- 输入数据的微小变化会得到完全不同的hash值,相同的数据会得到相同的值。
- 哈希算法的执行效率要高效,长文本也能快速地计算出哈希值。
- Hash算法的冲突概率要小
由于hash的原理是将输入空间的值映射成hash空间内,而hash值的空间远小于输入的空间。
根据抽屉原理,一定会存在不同的输入被映射成相同输出的情况。
抽屉原理:桌上有十个苹果,要把这是个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。这一现象就是我们所说的“抽屉原理”
2 HashMap原理
2.1 HashMap的继承体系是什么样的
2.2 Node数据结构分析
static class Node<K,V> implements Map.Entry<K,V> {
//key的哈希值
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;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
2.3 底层存储结构
- 当某一位置发生哈希碰撞且链表长度小于8时,数据按链表存储。
- 当链表长度超过8(长度>9)且哈希表中元素个数超过64的时候,链表结构升级为红黑树
2.4 put数据原理分析
- 路由寻址公式:(table.length - 1) & node.hash
2.5 什么是Hash碰撞
- 当经过路由寻址之后,所要插入的位置已经被占用,就被称为“hash碰撞”
2.6 什么是链化
- 当发生Hash碰撞后,所要插入的节点插入到该位置的节点之后,形成链表。称为“链化”
2.7 jdk8为什么引入红黑树
- 在发生多次Hash碰撞后,哈希表链化严重,链表长度过长时,则查找时间复杂度从O(1)降到O(n),效率变差。
- 引入红黑树之后,在链表长度及哈希表元素数量到达一定值的时候,将链表转化为红黑树,红黑树是一种平衡二叉树,可以极大地降低查询时的时间复杂度。
2.8 HashMap扩容原理
- 在一个哈希表中,存储元素数量过多时,即使使用红黑树,时间复杂度依然会趋向于O(n),此时,需要对该哈希表进行扩容,将原来的哈希表中的元素重新散列放到新的更大的哈希表中,用扩大占用空间的办法提升查找时效率。
3. 源码分析
3.1 HashMap核心属性分析
//默认的哈希表大小 = 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//默认哈希表最大容量 =
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子 = 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树化阈值1 = 8,且单个链表长度超过8,链表转化为树
static final int TREEIFY_THRESHOLD = 8;
//树化阈值2 = 64
// 当哈希表中所有元素个数大于64,才允许树化
static final int MIN_TREEIFY_CAPACITY = 64;
//链化阈值 = 6 ,当树中的元素个数少于该值,树降级为链表
static final int UNTREEIFY_THRESHOLD = 6;
//哈希散列表
transient Node<K,V>[] table;
//
transient Set<Map.Entry<K,V>> entrySet;
//当前哈希表中元素个数
transient int size;
//当前哈希表结构修改次数(插入、删除,修改除外)
transient int modCount;
//扩容阈值,当哈希表中的元素超过阈值时,触发扩容
int threshold;
//负载因子,默认0.75
//threshold = capacity * loadFactor
float loadFactor;
3.2 构造方法分析
public HashMap(int initialCapacity, float loadFactor) {
//参数校验
// initialCapacity取值范围为 (0,MAXIMUM_CAPACITY]
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//loadFactor取值范围为 [0,NaN)
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//设置负载因子
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
/**
* 返回一个大于等于当前cap的值,且该值是2的幂
*
* 假设 cap = 10
* n = 10-1 = 9 = 0b 1001
* n |= n >>> 1; 0b 1001 | 0b 0100 = 0b 1101
* n |= n >>> 2; 0b 1101 | 0b 0011 = 0b 1111
* n |= n >>> 4; 0b 1111 | 0b 0000 = 0b 1111
* n |= n >>> 8; 0b 1111 | 0b 0000 = 0b 1111
* n |= n >>> 16; 0b 1111 | 0b 0000 = 0b 1111 = 15
*
* return n+1; n+1 = 16
*
* 处理结果实例 : 一定是2的幂
* 0010 1011 1101 0110
* ---> 0011 1111 1111 1111 +1
* ---> 0100 0000 0000 0000
*/
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;
}
3.3 HashMap put方法、putVal方法分析
//向外暴露的put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 让key的hash值的高16位也参与路由运算
* 异或:相同则返回 0,不同则返回1
* h = 0b 0010 0101 1010 1100 0011 1111 0010 1110
*
* h ^ h>>>16
* 0b 0010 0101 1010 1100 0011 1111 0010 1110
* ^
* 0b 0000 0000 0000 0000 0010 0101 1010 1100
* =
* 0b 0010 0101 1010 1100 0001 1010 1000 0010
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 路由寻址算法: 散列表长度length key的hash值hash
* index = (length - 1) & hash
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//tab:引用当前hashMap的散列表
//p:表示当前散列表的元素
//n:表示散列表数组的长度
//i:表示路由寻址结果
Node<K,V>[] tab; Node<K,V> p; int n, i;
//懒加载初始化,第一次put时先初始化散列表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//寻址找到的桶中没有数据,直接将节点放在该位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//当前桶位中的元素和要插入的元素hash值相等,需要进行替换操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//若该桶位已经树化,则向树中插入元素
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//桶位中为链表,且头元素与要插入的元素不同
for (int binCount = 0; ; ++binCount) {
//若遍历到尾结点,则该链表中没有可替换元素
if ((e = p.next) == null) {
//将要插入的元素插入到链表尾部
p.next = newNode(hash, key, value, null);
//当链表长度超过树化阈值,则链表进行树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//找到了相同的元素,不需要进行插入
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//条件成立时,e为与被插入元素的key完全相同的结点
//进行值的替换
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//modCount:当前哈希表结构修改次数(插入、删除)
++modCount;
//当散列表元素数量大于扩容阈值,则进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
3.4 HashMap resize扩容方法分析
/**
* 为什么要扩容?
*
* 为了解决哈希冲突导致的链化严重而影响查询效率的问题,
* 通过扩容的办法,以空间换时间,缓解该问题
*/
final Node<K,V>[] resize() {
//oldTab:扩容前的哈希表
Node<K,V>[] oldTab = table;
//oldCap:扩容前哈希表的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//oldThr:扩容前的扩容阈值
int oldThr = threshold;
/**
* newCap:扩容后的长度
* newThr:扩容后的扩容阈值
*/
int newCap, newThr = 0;
//oldCap>0,说明hashMap已经初始化过了,是正常扩容
if (oldCap > 0) {
//扩容前大小超过hashMap最大容量,不再扩容,且设置新扩容阈值为最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//新的hashMap容量为原来的2倍
//新的容量小于最大容量 且 旧的容量大于16时,新的扩容阈值为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//oldCap == 0,说明hashMap还没初始化
//1. new HashMap(initCap,loadFactory)
//2. new HashMap(initCap)
//3. new HashMap(map) 且Map有数据
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// oldCap == 0 && oldThr == 0
// new HashMap()
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY; //16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //12
}
//newThr 为0时,通过newCap和loadFactor 计算出newThr
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; //更新threshold
//创建一个长度为newCap的新的数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node<>()[newCap];
table = newTab;
//oldTab!=null,则表示原数组已存在
if (oldTab != null) {
//循环遍历老数组
for (int j = 0; j < oldCap; ++j) {
//当前节点
Node<K,V> e;
//说明当前节点有数据
if ((e = oldTab[j]) != null) {
//清空原数组的结点,方便GC回收
oldTab[j] = null;
//当前节点为单个节点,将当前节点重新hash后放到新数组中
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
//Head:链表头 Tail:链表尾
//低位链表:再hash后下标和原下标相同的结点
Node<K,V> loHead = null, loTail = null;
//高位链表:再hash后下标 = 原下标 + 原数组长度
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
/**
* hash-> 0b 0001 1111 or 0b 0000 1111
* oldCap:0b 0001 0000 0b 0001 0000
* result:0b 0001 0000 or 0b 0000 0000
* 十进制 : 16 or 0
*
* 当结果为0时,放入低位链表中
* 结果不为0时,放入高位链表中
*/
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;
}
3.5 HashMap get方法分析
//向外暴露的get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
/**
* tab:当前HashMap的散列数组
* first:桶位中的头结点
* n:散列数组大小
*/
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//桶位中的头元素为要查找的元素,直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//桶位节点有next元素,说明当前桶位不止一个元素
if ((e = first.next) != null) {
//桶位中为红黑树
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//桶位中为链表
//e从头结点的后一个元素开始
//循环判断链表,返回符合key的结点
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
3.6 HashMap remove方法分析
//向外暴露的remove方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
/**
* tab:当前HashMap中的散列表
* p:当前node元素
* n:表示散列表数组长度
* index:表示寻址结果
*/
Node<K,V>[] tab; Node<K,V> p; int n, index;
//判断当前散列表存在,且key对应的桶位有数据。否则直接返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
/**
* node:查找到的结果
* e: 当前node的下一个元素
*/
Node<K,V> node = null, e; K k; V v;
//当前节点为要删除的目标节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//当前节点为链表或红黑树
else if ((e = p.next) != null) {
//当前节点树化,遍历红黑树找到目标节点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
//当前节点链化,遍历链表找到目标节点
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//当目标节点存在,且不匹配value或value匹配成功时
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//从树中删除目标节点
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//目标节点为链表头结点
else if (node == p)
tab[index] = node.next;
//目标节点为链表中间节点
else
p.next = node.next;
//操作数+1
++modCount;
//散列表元素数量-1
--size;
afterNodeRemoval(node);
//返回被删除的结点
return node;
}
}
return null;
}
3.7 HashMap replace方法分析
//通过key直接替换value的方法
public V replace(K key, V value) {
Node<K,V> e;
//通过key找到目标节点。没找到则返回null
if ((e = getNode(hash(key), key)) != null) {
//直接进行替换,并返回原来的值
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
//通过key和oldValue匹配,使用newValue替换oldValue
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}