HashMap是Java中非常常用的一个数据结构,它基于哈希表实现,提供了快速的插入、查找和删除操作。下面我们将基于JDK的源码,详细解析HashMap的工作原理,特别是put方法和扩容机制。
一、HashMap概述
HashMap内部使用一个数组(Node[] table)来保存数据,每个数组元素是一个链表(在JDK1.8后,链表长度大于8且数组长度大于64时,链表会转换为红黑树)。当发生哈希冲突时,数据会保存在链表中。HashMap的容量(capacity)指的是数组的大小,而HashMap的大小(size)指的是实际存储的键值对数量。
二、HashMap的put方法
当我们调用HashMap的put方法时,会执行以下步骤:
- 计算键的哈希值:首先,通过键的hashCode方法计算哈希值,然后对这个哈希值进行一些额外的运算,以减少哈希冲突。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 确定数组索引:然后,使用计算出的哈希值和数组长度来确定键值对应该保存在数组的哪个位置。
i = (n - 1) & hash; // n为数组长度
这里使用了位运算来快速计算索引。 3. 处理哈希冲突:如果计算出的索引位置已经有数据,那么就需要处理哈希冲突。HashMap使用链表(或红黑树)来保存具有相同哈希值的键值对。 4. 添加或更新键值对:如果键已经存在,则更新对应的值。否则,创建一个新的节点并添加到链表中。
三、HashMap的扩容
当HashMap的大小达到容量的一定比例时,就会进行扩容。扩容的目的是减少哈希冲突,提高查询效率。
- 扩容时机:当HashMap的大小达到容量与加载因子(load factor,默认值为0.75)的乘积时,就会触发扩容。
if (++size > threshold)
resize();
其中,threshold是扩容的阈值,它的值等于容量与加载因子的乘积。 2. 创建新数组:扩容时,会创建一个新的数组,其大小是原数组的两倍。
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
- 重新计算索引并移动数据:然后,遍历原数组中的每个元素,重新计算它们在新数组中的索引,并将它们移动到新数组中。
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)
... // 红黑树的处理逻辑
else { // 链表的处理逻辑
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
... // 根据哈希值重新分配节点到新数组
} while ((e = next) != null);
... // 将链表连接到新数组
}
}
}
在这个过程中,由于数组的容量变大了,所以相同的哈希值在新数组中可能会计算出不同的索引,从而减少哈希冲突。
总结
HashMap通过哈希表和链表(或红黑树)的结合,实现了高效的插入、查找和删除操作。在插入数据时,HashMap会根据键的哈希值将数据保存在数组的特定位置。当发生哈希冲突时,数据会保存在链表中。当HashMap的大小达到一定比例时,会进行扩容,以减少哈希冲突,提高查询效率。