想要了解HashMap集合,先来了解其中的哈希值计算原理
为何哈希?
Hash也称散列、哈希,其中基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出。这个映射的规则就是对应的Hash算法,而这个输入出后的固定长度的结果(二进制串)就是哈希值。但是由于通过Hash算法产生的哈希值是有限的,而存储的数据可以说是无限的。这样就可能会导致存储的数据不同,而两者产生的哈希值却相同,此情况称为哈希冲突。
HashMap的如何解决哈希冲突?
为了更好的解决这个问题,我们先来了解HashMap类中的基本信息属性和数据结构。
// 哈希表,数组结构
transient Node<K,V>[] table;
// 默认的哈希表数组容量大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
// 哈希表数组最大容量
static final int MAXIMUM_CAPACITY = 1 << 30; // 1073741824
// 默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 通过map的构造方法手动指定的负载因子
final float loadFactor;
// 哈希表扩容临界值,它的值 = 哈希表当前容量 * 负载因子。当map中的存放的键值对大于threshold时,则进行数组扩容
int threshold;
// 针对于链表来说,树化(即链表转换成红黑树)的临界值。当链表中的结点数量大于等于8时,满足树化的条件之一
static final int TREEIFY_THRESHOLD = 8;
// 解除树化的临界值,即红黑树转换回链表
static final int UNTREEIFY_THRESHOLD = 6;
// 针对于哈希表来说,树化(即链表转换成红黑树)的临界值。当哈希表中的容量大于等于64时,满足树化的条件之一
static final int MIN_TREEIFY_CAPACITY = 64;
// map中的key集合
transient Set<Map.Entry<K,V>> entrySet;
// 记录map中元素数量
transient int size;
// 记录map中存储结构改变次数
transient int modCount;
以上属性便是HashMap中的基本信息,但只是单单查看这几个属性是不足以了解HashMap的内部结构的。为此,请看下图:
通过结合属性讲解和观察上图,相信大家都对HashMap的内部存储结构有了初步的了解。为了更加深入HashMap,我们通过下图再来看下它是如何决定数据保存的位置的:
到此,可以得知HashMap中就是通过链表或者红黑树的方式解决哈希冲突问题。
哈希表table为何要扩容?
从以上内容可以了解到,哈希冲突的问题可以通过转换成链表或者红黑树结构就可以解决。换个角度想,只有哈希表table是数组,有长度的限制,但是链表和红黑树的话,其中的结点只通过前后指针的指向来寻找前任或后继节点,不存在长度限制问题。
也就是说,即使发生了很多哈希冲突,只要遵循HashMap的存储数据规则,在硬件条件允许的情况下,就不存在数据溢出的问题。那么为什么还要对哈希表进行扩容处理呢?
这就要说到,数组和链表的特性了。
- 数组:查询快、增删慢
- 链表:查询慢、增删快
要知道,之所以采用数组+链表+红黑树这样的实现结构,就是为了提高键值对数据存取的效率(链表树化也是同样的道理)。
如果哈希表不扩容的话,默认的容量只有16,当数据量一大,这区区的16就变得微不足道了。而此时,整体的结构除了那只拥有16个长度的哈希表之外,就只有链表了,那么又回到了链表的查询效率过慢的问题。
因此,为了避免这种情况,哈希表扩容志在必行。
HashMap中的链表内部类Node
在了解底层方法的源码之前,先来了解下其中的数据的封装形式——链表结点。
(当然还有红黑树结点,但是要了解红黑树的话,就要从红黑树的各种特性开始讲起,这样又是另一篇博文了。因此在此只了解链表的具体实现,对于红黑树的实现,在此文中不做详细的说明)
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 该结点中key所生成的hash值
final K key; // key值
V value; // value值
Node<K,V> next; // 指向下一个拥有同样hash值的结点
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; }
/**
* 获取该结点的hash码
*/
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
/**
* 设置新的value值,并返回旧的value值
*/
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
/**
* 比较指定结点的key、value是否相等
*/
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;
}
}
HashMap常用重点方法源码解析:
put——添加键值对
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true); // 添加键值对
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // 返回由key所生成的hash值
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) // 哈希table为空,或者长度为0,说明未初始化
n = (tab = resize()).length; // 初始化哈希table,并返回容量长度
if ((p = tab[i = (n - 1) & hash]) == null) // 根据路由算法计算数据存储索引位置,且索引位置尚无数据
tab[i] = newNode(hash, key, value, null); // 将该数据添加到索引位置,添加数据成功,结束
else { // 索引位置已经有数据了,说明出现哈希冲突
Node<K,V> e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 与已存在节点p做比较,hash值、key值相等(之后会进行value值替换)
e = p; // 临时结点e指向已存在的结点p
else if (p instanceof TreeNode) // 已存在的结点p的类型为红黑树结点类型,说明该链表已树化
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 调用红黑树添加结点的方法
else { // 否则,需要将数据节点添加到链表末尾
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { // 结点p的后继结点为空,说明p为末尾结点
p.next = newNode(hash, key, value, null); // 添加新节点作为p的后继结点
if (binCount >= TREEIFY_THRESHOLD - 1) // 此时判断,链表的长度大于等树化临界值
treeifyBin(tab, hash); // 链表树化
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) // 在链表中找到与hash值、key值相等的节点,跳出循环(之后会进行value值替换)
break;
p = e;
}
}
if (e != null) { // 临时节点e不为空,说明存在hash值、key值相等的节点,则替换value值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold) // map中存放的键值对数量大于扩容临界值
resize(); // 哈希表扩容
afterNodeInsertion(evict);
return null;
}
以上put方法添加数据的源码,搭配下图理解效果更佳:
treeifyBin——树化链表
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // 此处判断是重点。当哈希表的容量小于树化临界值,说明无须树化(因此,树化的条件有两个:链表长度超8;哈希表容量超64)
resize(); // 扩容即可
else if ((e = tab[index = (n - 1) & hash]) != null) { // 否则,说明哈希表的容量已超过树化临界值,进行树化
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
resize——调整哈希表table容量
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) { // 原哈希表table容量oldCap大于0,说明哈希表已初始化,进行哈希表扩容
if (oldCap >= MAXIMUM_CAPACITY) { // oldCap大于指定的最大容量
threshold = Integer.MAX_VALUE; // 扩容临界值只能给整型的最大值了
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) // oldCap大于等于默认容量,而且将其增加2倍后仍小于指定最大容量
newThr = oldThr << 1; // 则将扩容临界值增加2倍
}
else if (oldThr > 0) // oldCap小于等于0,说明哈希表未初始化
newCap = oldThr; // 将新容量设置为原扩容临界值
else { // 否则,则使用默认的容量和扩容临界值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) { // 新扩容临界值为0,则使用 新容量 * 负载因子 作为新扩容临界值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; // 使用新的扩容临界值
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; // 哈希表扩容成新数组(此时新哈希表中是没有数据的,需要从原哈希表中迁移过来)
if (oldTab != null) { // oldTab不为空,说明原哈希表已初始化过,则将原哈希表中的数据全部移动到新哈希表中
for (int j = 0; j < oldCap; ++j) { // 循环原哈希表中的结点
Node<K,V> e;
if ((e = oldTab[j]) != null) { // 原哈希表中的元素不为空,则将元素作为临时节点e保留下来
oldTab[j] = null; // 清空原哈希表
if (e.next == null) // 结点e的后继结点为空,说明该位置没有出现过哈希冲突
newTab[e.hash & (newCap - 1)] = e; // 则直接将该结点添加到新哈希表即可
else if (e instanceof TreeNode) // 结点e类型为红黑树结点,说明该链表已经树化
((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 则调用红黑树迁移结点方法
else { // 否则,说明需要迁移整条链表的结点
/**
* 此处需要额外说明:因为迁移整条链表的话,并不是我们想象中的那样,整条链表原封不动的迁移
* 而实际上,在链表迁移的过程中,它已经被拆分成两条新的链表进行添加
* 在这里,我们称这两条链表为lo链表、hi链表
* 想知道具体原因的朋友可以访问如下链接:
* https://blog.youkuaiyun.com/weixin_41565013/article/details/93190786
*/
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) { // 此处判断等于0,说明节点e属于lo链表,使用尾插法将其插入lo链表中
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else { // 否则,属于hi链表,使用尾插法将其插入hi链表中
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 最后将lo链表、hi链表添加到新哈希表中
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
get——根据key值获取value值
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) {
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) { // 通过路由算法找到对应的结点first
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k)))) // 先查看firs结点是否是要找的目标结点
return first; // 找到直接返回
if ((e = first.next) != null) { // first不是要找的目标结点,则继续往后继结点寻找,找到后返回
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null; // 找不到则返回空
}
remove——根据key值删除元素
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) { // 通过路由算法找到对应的结点p
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) // 首先查看结点p是否是要删除的目标结点
node = p; // 找到后,则用临时结点node指向链表头结点
else if ((e = p.next) != null) { // 结点p不是目标结点,则继续往后继结点寻找,找到后用临时结点node指向目标结点
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);
}
}
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;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null; // 找不到目标结点,删除失败,返回空
}
到此,HashMap的源码解析就结束了。本文针对了HashMap的原理,以及增、删、查方法进行源码的解析,其他剩余的方法就不一一列举讲解了。如果能够看懂这篇博文,那么就可以举一反三,一通百通了。