在 Java 中,HashMap
是最常用的集合类之一,它实现了 Map
接口,用于存储键值对(key-value)映射。通过哈希表实现,提供了常数时间的查找性能,通常在大多数情况下能高效地存储和检索数据。本文将深入分析 HashMap
的底层实现、方法、扩容机制、以及它的主要特点,帮助大家更好地理解和使用 HashMap
。
1. HashMap
的基本构成
HashMap
采用哈希表(数组 + 链表或红黑树)来存储数据,每个元素都是一个 Entry
对象,包含键(key)、值(value)以及指向下一个 Entry
对象的指针。
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;
}
}
HashMap
底层数据结构使用了一个 Entry
数组,数组中的每一个元素都是一个链表的头节点,或者在高负载情况下,转变为红黑树。
2. HashMap
的基本操作
-
put(K key, V value)
:向HashMap
中插入一个键值对。 -
get(Object key)
:通过键获取对应的值。 -
remove(Object key)
:删除某个键值对。 -
containsKey(Object key)
:判断是否包含某个键。 -
size()
:获取HashMap
中元素的个数。
3. HashMap
的底层实现原理
HashMap
内部通过数组和链表(或红黑树)实现存储。它的底层原理可以分为以下几个步骤:
3.1. 哈希算法
HashMap
使用哈希算法将键映射到数组的索引位置。计算过程为:
int hash = (key == null) ? 0 : hashCode(key.hashCode());
int index = (hash & (array.length - 1));
这里,hashCode()
方法计算出键的哈希值,然后对数组的长度进行掩码操作,确定元素在数组中的位置。
3.2. 处理哈希冲突
当两个或多个键经过哈希算法计算出相同的索引位置时,就发生了哈希冲突。HashMap
通过链表或红黑树来解决哈希冲突:
-
链表法:如果链表长度较短(通常小于 8),
HashMap
会采用链表结构,将冲突的元素添加到链表中。 -
红黑树法:当链表长度较长时(超过 8),
HashMap
会将链表转换为红黑树,以优化查找性能。
3.3. put
方法解析
put
方法在添加元素时,首先根据键计算出哈希值,然后查找对应数组的索引位置。如果该位置为空,直接存入元素;如果已有元素,就通过链表或者红黑树解决冲突。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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;
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;
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;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
3.4. 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) {
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;
if ((e = first.next) != null) {
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;
}
4. HashMap
的扩容机制
HashMap
的容量是动态变化的,当元素数量超过负载因子(默认值为 0.75)时,HashMap
会进行扩容,通常是将数组大小翻倍,并重新计算每个元素的哈希值来保证性能。
4.1. 扩容过程
扩容的核心步骤是创建一个新的数组,并将原数组中的元素重新计算哈希值并插入新的数组。这个过程可能会引起性能下降,因为需要重新分配内存和重新计算哈希值。
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;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 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) {
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 { // 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;
}
}
}
}
}
return newTab;
}
4.2. 扩容时的影响
扩容时,所有的元素都会被重新哈希,在插入大量数据时,扩容可能会导致性能波动。
5. HashMap
的特点
-
查找效率高:哈希表的查找是基于哈希值直接定位的。
-
线程不安全:
HashMap
是非线程安全的,在多线程环境下使用时需要考虑同步机制(例如ConcurrentHashMap
)。 -
允许键和值为 null:
HashMap
允许一个键为 null 和多个值为 null。 -
迭代顺序不保证:
HashMap
中元素的顺序是不固定的,不保证插入顺序。对于有序的需求,可以考虑使用LinkedHashMap
。
6. 总结
HashMap
是 Java 中常用的集合类,具有高效的查找性能,适用于存储大量键值对数据。它的底层实现基于哈希表,采用链表或红黑树解决哈希冲突,并通过扩容来提高存储容量。虽然 HashMap
提供了高效的操作,但它并不是线程安全的,需要在并发环境下使用时考虑额外的同步机制。
希望这篇文章能够帮助你深入理解 HashMap
的底层原理及其特点,并在实际开发中更好地使用它。