在Java编程中,HashMap是一个非常常用的数据结构,它提供了高效的键值对存储和检索操作。作为Java集合框架的一部分,HashMap在JDK 1.8版本中经历了一系列改进和优化。本文将深入研究JDK 1.8中HashMap的源代码,探索其内部工作原理以及关键方法的实现。
HashMap是一种基于哈希表的数据结构,通过使用哈希函数计算键的哈希码,将键值对存储在内部数组中的特定位置。这样可以大大加快元素的查找、插入和删除速度。此外,HashMap还允许null作为键和值,并且可以处理键的冲突情况。
通过深入分析JDK 1.8中HashMap的源代码,我们将更好地理解HashMap的内部机制和工作原理。这将为我们在实际开发中选择和使用HashMap提供宝贵的指导,并且对于深入理解Java集合框架中的哈希表实现也会大有裨益。
请继续阅读,让我们一起开始这段令人兴奋的探索之旅吧!
HashMap<String, Object> map = new HashMap<>();
map.put("Aa", "Aa");
map.put("BB", "BB");
System.out.println(map);
当我们向HashMap中put数据的时候首先会根据key计算一个hash值。
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);
}
为什么hash函数这么设计?
在JDK 1.8中,HashMap使用了一种称为"扰动函数"(also known as "hashing function")的机制来计算键的哈希码。这个扰动函数的目的是将键的哈希码更均匀地分布在内部数组的索引范围内,以减少冲突并提高性能。
让我们逐步解释这个扰动函数的作用:
首先,检查键是否为null。如果键为null,则返回哈希值0。这是为了处理特殊情况,并避免NPE(NullPointerException)。
如果键不为null,那么调用键的hashCode()方法来获取初始哈希码。
接下来,对初始哈希码进行一个位运算操作。具体来说,执行h ^ (h >>> 16)。这是一个异或操作(XOR),它将哈希码的高16位与低16位进行混合和扰动。这个操作的目的是增加一些额外的位变化,以便使最终的哈希码分布更加均匀。
通过使用这个扰动函数,HashMap可以更好地处理键的哈希冲突,减少链表长度和查找时间,并提高整体性能。
需要注意的是,扰动函数的实现可能在不同的JDK版本中有所变化。这是因为Java的哈希算法可以根据实现版本进行调整和改进,以适应不同类型的键和具体需求。
通过了解HashMap中使用的扰动函数,我们可以更好地理解它如何影响键的哈希码计算,从而对HashMap的性能和行为有更深入的理解。
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;
}
在putVal方法中首先判断table是否为null,如果为null则调用resize()进行初始化
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;
}
HashMap类中的DEFAULT_INITIAL_CAPACITY表示默认的初始容量,而DEFAULT_LOAD_FACTOR表示默认的负载因子。
DEFAULT_INITIAL_CAPACITY:在JDK 1.8中,HashMap的默认初始容量被设置为16。这意味着在创建HashMap实例时,如果没有显式指定初始容量,则会使用默认值16。初始容量是哈希表内部数组的大小,它决定了HashMap最初可以存储键值对的数量。
DEFAULT_LOAD_FACTOR:在JDK 1.8中,HashMap的默认负载因子被设置为0.75。负载因子是一个衡量HashMap何时进行扩容的因素。当HashMap中的元素数量超过等于负载因子与当前容量的乘积时,就会触发扩容操作。例如,默认的负载因子0.75和初始容量16意味着,当HashMap中的元素数量达到或超过12(0.75 * 16)时,就会自动进行扩容以维持性能。
选择适当的初始容量和负载因子可以影响HashMap的性能。较小的初始容量可能会导致更频繁的扩容,而较大的初始容量可能会浪费内存。合理调整负载因子可以平衡空间利用率和查找效率之间的权衡。
需要注意的是,如果需要在创建HashMap时使用不同的初始容量和负载因子,可以使用构造函数或者调用相应的putAll方法来实现。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
初始化完成之后就可以向table中添加数据了,其中关键点:
if((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
在HashMap中,[i = (n - 1) & hash]
是用来计算键的哈希码对应的索引位置的表达式。这个表达式通常在插入、获取或删除元素时使用。
让我们逐步解释这个表达式的含义:
n
表示HashMap的容量,是内部数组的长度。hash
表示键的哈希码。
整个表达式的意义是将哈希码与当前容量进行位运算,以得到键应该存储在内部数组的哪个索引位置上。
具体而言,(n - 1) & hash
的运算过程如下:
- 首先,
n - 1
对当前容量进行减一操作,目的是确保结果范围在 0 到 n-1 之间,即在内部数组的有效索引范围内。 - 然后,使用位与(AND)操作符
&
将哈希码和容量减一的结果进行位运算。这个位与运算可以保证最终的索引值落在 0 到 n-1 的范围内。
通过使用 (n - 1) & hash
表达式,可以有效地将键的哈希码映射到内部数组的索引位置。由于采用位运算,它比使用取模运算 (%
) 更高效。此外,通过使用容量为2的整数次幂的HashMap,可以确保位运算的结果是均匀分布在数组索引范围内的,从而减少冲突。
if(binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
在JDK1.8中引入了红黑树,当链表长度大于等于TREEIFY_THRESHOLD - 1,并且数组长度大于等于MIN_TREEIFY_CAPACITY是就会转红黑树。
static final int TREEIFY_THRESHOLD = 8;
static final int MIN_TREEIFY_CAPACITY = 64;
当链表长度小于等于UNTREEIFY_THRESHOLD时则会转回链表,但是只在resize()方法中触发
static final int UNTREEIFY_THRESHOLD = 6;