一、简介
HashMap是可以实现高效访问的数据结构,不同于数组和链表,数组是查询方便、增删复杂;链表是增删方便,查询复杂;HashMap很好的结合了二者的优点。
在添加时,如果计算的哈希值不重复,就直接放入数组,简单高效;而当哈希值重复了,则会按照链表的方式插入,这恰号用到了链表的优点。查询时,可以快速按照索引定位到某一个位置,而如果某个索引上有多个元素,在元素不多的时候按照链表的方式查询,效率也可以接收,当元素很多时,可以变成红黑树,同样时间复杂度降低,可以说HashMap真的是大佬们智慧的结晶。
二、底层实现原理
1、HashMap的底层是什么
如今,面试的过程中HashMap的底层实现原理可以说是一个合格程序员必须了解的知识,但是还是有必要记录以下,便于后面复习。
这里为了方便就截取有用的源码部分了(本来也不是给大家看的,如果有谁恰好看到了,可以自行查看相关源码的具体实现,这里是东拼西凑的代码),首先看下面这一段:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
...
}
transient Node<K,V>[] table;
}
上面是说HashMap类,首先继承了AbstractMap类,AbstractMap类又实现了Map接口,可见Map这一家族和Collection家族并不属于同一个大的家族。
HashMap的底层有一个静态内部类Node<K,V>,这相当于就是一个又一个的节点了,这个静态内部类实现了Map.Entry<K,V>接口。
在Map.Entry中我们可以看到下面代码(依然只截取有用的部分):
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
int hashCode();
}
里面写了一些最最常用的方法,计算hash值,获取key、value等。
另外,不难看出来,HashMap的底层有一个Node节点的数组,名为table,这也就是我们常说的数组 + 链表的那个数组table了。
2、HashMap的基本参数
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//这是初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//加载因子0.75,当达到数组容量的75%就开始扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表长度大于8以后会树化(当然table大小要大于64才会树化),也就是变成红黑树
static final int TREEIFY_THRESHOLD = 8;
//只有链表长度变为6以后才会回到链表状态,避免在长度为8之间来回增删元素,来回树化、链表化
static final int UNTREEIFY_THRESHOLD = 6;
//最小树化容量
static final int MIN_TREEIFY_CAPACITY = 64;
3、HashMap的源码分析:放入元素的过程
测试的用例:
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("key1","value1");
map.put("key2","value2");
map.put("key1","value3");
map.put("key4","value4");
map.put("key5","value5");
map.put("key6","value6");
map.put("key7","value7");
map.put("key8","value8");
map.put("key9","value9");
map.put("key10","value10");
map.put("key11","value11");
map.put("key12","value12");
map.put("key13","value13");
map.put("key14","value14");
map.put("key15","value15");
map.put("key16","value16");
}
首先使用Debug进入HashMap,会先进入:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
这相当于是使用创建HashMap时,设置了默认加载因子0.75,DEFAULT_LOAD_FACTOR也是上面的一个参数。
因为往map中放入了key和value,所以调用了:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
进入putVal()方法:
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;
}
一段一段看,因为最开始创建map,没有初始化table,所以(tab = table) == null肯定是成立的,所以会走下面一个分支:n = (tab = resize()).length;
此时的resize()方法就等于是初始化了,简单看一下resize()的源码片段:
//新的默认初始化容量是16
newCap = DEFAULT_INITIAL_CAPACITY;
//新的扩容容量是16 * 0.75 = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
//初始化一个新的数组,长度是16
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
接着看putVal的下一行:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
这里说的是经过计算在table中找到了一个位置来放元素,如果这个位置没有元素,那么就创建一个新的节点,把这个值放入,这里去查看就能发现已经有一个元素了。
然后看下面一行:
if (++size > threshold)
resize();
上面两行的作用是判断是否要做扩容,size初始值为0,++size变为1,threshold就是扩容的值,这里是12,肯定是不满足条件的,所以不会调用resize()。
这样一个值就放好了,现在继续put元素,还是会进入putVal()方法:
这次是因为table已经初始化了,所以不会进入第一个if,而是进入下面一个的if语句,还是计算位置,放入元素。
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);
4、HashMap的源码分析:放入相同key的情况
执行到put的第三行,key = key1,已经存在了元素,还是会进入下面这行:
计算出当前key的哈希,去table里找,发现里面不为空,会进入else分支。
if ((p = tab[i = (n - 1) & hash]) == null)
...
else {
...
下面就是存放一个key的hash值相同元素的情况了:
- 第一种情况,当hash值相同,且key值也和原来的值一模一样,说明是要做value的替换,这个时候就会进入最下面的if语句中,使用e.value = value来替换元素
- 第二种情况,如果这个也就是说不相等,这个时候要添加元素了,当该列已经树化了,那么直接调用putTreeVal()即可
- 第三种情况比较复杂,如果是要添加元素,而此时不是树的情况,就需要考虑链表的长度是否会造成树化了,还有链表当中存不存在这个key了,如果大于8了,会变成红黑树,调用treeifyBin()方法,如果链表当中的哈希值相同,并且key也相同,就会进行替换
- 如果p.next为空说明找到了链表最后一个位置,然后
p.next = newNode()添加新元素即可,还要考虑是否达到了TREEIFY_THRESHOLD,达到了树化,如果不满足条件,说明没遍历到最后一个元素,这个时候让p = e,也就是p = p.next,继续向后遍历即可
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;
}
5、HashMap的源码分析:数组需要扩容的时候
前12个应该都没问题,可以看看添加的元素到table的位置。
创建新数组阶段
当执行到第十三个的时候,putVal()方法中:
++modCount;
if (++size > threshold)
resize();
这里的threshold = 12,而++size会使得size变为13,从而调取resize()方法。
看看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;
}
首先会进入第一行:
因为oldTab就是最开始创建的表,它的长度就是16,所以oldCap = 16;而oldThr = threshold 就是刚才进行比较的值12。
int oldCap = (oldTab == null) ? 0 : oldTab.length; //16
int oldThr = threshold; //12
接下来会进入第一个条件判断:
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分支就是把oldCap扩大一倍,oldThr也扩大一倍,也就是数组长度变为32(16 × 2),数组扩容的临界值变为24(12 × 2)。
后面有几步都是赋值操作,不需要看,后面的代码是:
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
这样就把数组创建出来了。
旧数组元素移动阶段
再下面就是移动阶段了,如果oldTable 不等于空,说明有元素,那么遍历旧数组,看看哪个位置的元素不为0。
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) { //遍历数组,看看哪个位置不为0
找到不为0的元素,把旧位置变为0,看看这个元素是什么类型的,如果e.next == null,说明它不是链表或者红黑树,仅仅是单独的元素,那就计算其所在新数组的位置放入即可。
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;
}
}