HashMap 是什么?
HashMap
是 Java 中常用的集合类,属于 Map
接口的实现,用来存储键值对。它的特点是基于哈希表实现,具备以下特性:
- 键值对的存储 :每个键(
key
)对应一个值(value
)。 - 快速查询 :通过哈希算法进行快速查询,
HashMap
查找、插入、删除操作的时间复杂度在理想情况下是 O(1)。 - 允许 null 值 :
HashMap
允许键和值为null
,但键只能有一个null
。
HashMap 的底层结构
HashMap
底层是基于数组 + 链表 + 红黑树的组合实现的,主要包括以下几部分:
- 数组(Node<K,V>[] table) :
HashMap
的核心是一个数组,数组中的每个元素是一个链表或树的头节点,称为"桶"。 - 链表:当多个键通过哈希碰撞(Hash Collision)映射到同一个位置时,这些键会以链表的形式存储。
- 红黑树:当链表长度超过一定阈值(默认是8)时,链表会转换成红黑树,以提高查询效率。
HashMap 的工作原理
-
存储过程:
- 通过
hash(key)
方法计算键的哈希值,并通过indexFor(hash, table.length)
方法确定该键值对应的数组索引。 - 如果该索引位置没有元素,则将键值对直接存入该位置。
- 如果发生哈希碰撞(两个不同的键计算出相同的索引),则将新键值对以链表的形式追加到该索引的链表中。
- 当链表长度超过阈值 8 时,链表会转化为红黑树。
- 通过
-
扩容:
HashMap
的初始容量默认为 16,负载因子为 0.75。当数组元素达到容量的 75% 时(即threshold = capacity * load factor
),HashMap
会自动扩容,将数组容量翻倍,并重新计算每个元素的位置。
-
查找过程:
- 通过键的哈希值确定索引位置。
- 若数组中对应位置存在链表或红黑树,则根据键的
equals()
方法遍历链表或树,找到对应的键值对。
HashMap 的重要特性
- 哈希冲突:即两个或多个不同的键计算出相同的哈希值,这会导致多个键映射到数组的同一个索引。通过链表或红黑树解决冲突问题。
- 红黑树优化:当链表长度超过 8 时,链表会转化为红黑树,查找效率从 O(n) 提高到 O(log n)。
- 线程不安全 :
HashMap
是非线程安全的。如果需要在多线程环境下使用HashMap
,需要通过同步机制(如Collections.synchronizedMap()
)或使用线程安全的类(如ConcurrentHashMap
)。
红黑树的特点
红黑树是 HashMap 在链表长度超过一定阈值时使用的树结构。它是一种自平衡的二叉搜索树,具有以下特性:
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色。
- 每个叶子节点都是黑色的空节点(NIL 节点)。
- 红色节点的子节点必须是黑色的(不能有两个连续的红色节点)。
- 从任意节点到其每个叶子节点的所有路径都包含相同数量的黑色节点。
红黑树通过这些规则保证了树的平衡性,使得最坏情况下的查找、插入、删除操作的时间复杂度为 O(log n)。
HaspMap底层源码分析
HashMap
是 Java 中常用的数据结构,它以键值对 的形式存储数据,并通过哈希表 的形式快速查找键对应的值。为了更深入地理解 HashMap
的实现机制,我们需要从它的底层源码出发,分析其数据结构、核心方法和优化策略。
1. 基本数据结构
在 HashMap
中,最核心的两个数据结构是:
- 数组 :
Node<K,V>[] table
,这是HashMap
的底层存储结构,所有键值对都存储在数组中。 - 链表和红黑树:当出现哈希冲突时,多个键值对会被存储在同一个数组位置(桶)中,最初使用链表实现,当链表长度超过一定阈值时(8),会转换为红黑树。
java
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; // 指向下一个节点
}
每个 Node
节点包含 4 个属性:
hash
: 键的哈希值,用来确定元素存储的位置。key
: 键。value
: 值。next
: 指向下一个节点(用于解决哈希冲突时的链表)。
2. 核心字段
- DEFAULT_INITIAL_CAPACITY:默认初始容量为 16。
- DEFAULT_LOAD_FACTOR:默认负载因子为 0.75。
- threshold :扩容阈值,等于
容量 * 负载因子
,当元素数量超过该值时触发扩容。 - size :当前
HashMap
中键值对的数量。
3. 核心方法
3.1. put(K key, V value)
------ 插入数据
put
方法用于将键值对存入 HashMap
中。如果键已经存在,则更新对应的值;如果键不存在,则新插入一条记录。
java
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
hash(key)
:通过计算哈希值,将键均匀分布在数组的不同位置。putVal
:执行实际的插入逻辑。
putVal
方法逻辑
java
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)
n = (tab = resize()).length; // 初始化或扩容
// 计算索引 i,并判断 i 位置是否已有节点
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))))
e = p; // key 已经存在,记录节点
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) // 链表长度 >= 8,转换为红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break; // 处理key重复的情况
p = e;
}
}
// key重复时,更新旧值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize(); // 如果 size 超过阈值,进行扩容
return null;
}
- 计算哈希值和索引 :通过
(n - 1) & hash
计算数组中的位置(i
)。 - 存储键值对 :如果位置
i
为空,直接存储;否则通过链表或红黑树处理哈希冲突。 - 扩容 :如果元素数量超过阈值,则调用
resize()
方法进行扩容。
3.2. resize()
------ 扩容
扩容是 HashMap
中一个重要的机制。每次扩容时,容量会翻倍,旧数组中的元素会重新分配到新的数组位置上。
java
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) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
newCap = oldCap << 1; // 容量翻倍
newThr = oldThr << 1; // 阈值翻倍
} else if (oldThr > 0) {
newCap = oldThr;
} else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 重新哈希并迁移旧数据
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = 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 {
// 链表的重新分配
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 (loHead != null) newTab[j] = loHead;
if (hiHead != null) newTab[j + oldCap] = hiHead;
}
}
}
}
threshold = newThr;
return newTab;
}
扩容流程:
- 新建一个容量为旧容量 2 倍的数组。
- 遍历旧数组,将旧数据重新计算索引并迁移到新数组中。
- 扩容后,哈希冲突的节点会重新分布,减少链表长度。
4. 红黑树转换
HashMap
在解决哈希冲突时,采用链表存储冲突的键值对。当链表长度过长时(超过 8),为了优化查询性能,会将链表转换成红黑树。
java
void treeifyBin(Node<K,V>[] tab, int hash) {
// 转换链表为红黑树的逻辑
}
红黑树的优势在于即使在最坏情况下,插入、删除和查找的时间复杂度都为 O(log n),显著提高了性能。
5. 总结
HashMap
通过数组、链表和红黑树实现键值对的存储,能够在大部分情况下提供 O(1) 的查询和插入效率。- 哈希冲突通过链表解决,链表过长时通过红黑树优化性能。
HashMap
采用负载因子和扩容机制,以减少哈希冲突并保持高效操作。- 红黑树的引入避免了极端情况下链表过长导致的性能问题,保证了
HashMap
的高效性。
寻址算法
HashMap
的寻址算法是通过计算键的哈希值来定位键值对在数组中的存储位置。这个过程可以分为三个主要步骤:
1. 计算哈希值
每个键(key
)都有自己的哈希码 (hashCode
),这是一个整数。HashMap
通过 hash()
方法对键的 hashCode()
进行处理,使哈希值更加均匀分布,减少冲突。具体做法是通过扰动函数将哈希值的高位和低位进行混合:
java
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
key.hashCode()
计算键的哈希码。h ^ (h >>> 16)
:通过异或运算,将哈希值的高位和低位混合,确保更多位数参与索引计算,减少哈希冲突。
2. 计算数组索引
计算出哈希值之后,HashMap
通过位运算将哈希值映射到数组的索引上,公式为:
java
index = (n - 1) & hash
n
是HashMap
数组的长度,通常是 2 的幂(如 16、32 等)。n - 1
生成一个掩码,它确保索引在数组的有效范围内。&
按位与操作,只保留哈希值的低位,使其落在[0, n-1]
的范围内,得到一个有效的数组索引。
举例 :如果数组长度 n = 16
,某个键的 hash()
计算结果为 hash = 357
,则 index = (16 - 1) & 357 = 15 & 357 = 5
,所以该键值对会存储在数组索引为 5 的位置。
3. 处理哈希冲突
当两个不同的键计算出的索引相同,发生了哈希冲突 ,HashMap
使用两种方式解决:
- 链表:在同一个索引位置,以链表形式存储多个键值对。当插入时新元素会加到链表尾部,查找时遍历链表。
- 红黑树:如果链表长度超过阈值(默认8),会将链表转换为红黑树,提升查找、插入、删除的效率。
4. 扩容与再哈希
当 HashMap
的容量达到一定比例(默认是负载因子 0.75
),会进行扩容 ,即将数组长度加倍,并重新计算每个键值对的新位置。这一过程被称为再哈希,目的是减少哈希冲突,提高性能。
逻辑总结
- 计算哈希值 :根据键的
hashCode()
通过扰动函数计算出哈希值。 - 映射到数组索引 :通过
(n - 1) & hash
计算出键值对的存储位置。 - 处理哈希冲突:若冲突,使用链表或红黑树解决。
- 扩容与再哈希 :当达到负载因子时,
HashMap
扩容并重新计算索引位置。