引言:更多相关请看 JAVA并发编程系列
概述
HashMap最早是在jdk1.2中开始出现的,一直到jdk1.7一致没有太大的变化。到jdk1.8有一个很大的改动,其中一个最显著的改动就是:之前jdk1.7的存储结构是数组+链表(单),到了jdk1.8变成了数组+链表(单)+红黑树。目的是为了提高检索效率。另外,HashMap是非线程安全的,在多个线程同时对HashMap中的某个元素进行增删改操作的时候,是不能保证数据的一致性的。HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型。随着JDK(Java Developmet Kit)版本的更新,JDK1.8对HashMap底层的实现进行了优化,例如引入红黑树的数据结构和扩容的优化等。允许一个null键,多个null值。
底层数据结构
jdk1.7存储结构图

jdk1.8存储结构图

红黑树
红黑树是一个自平衡、基数节点是黑色、偶数节点是红色的二叉查找树,红黑树的查找效率是非常的高,查找效率会从链表的o(n)降低为o(logn)。如果之前没有了解过红黑树的话,也没关系,你就记住红黑树的查找效率很高就OK了。
为什么非要等到链表的长度>=8时才转变成红黑树
(1)构造红黑树要比构造链表复杂,在链表的节点不多时,从整体的性能来看,数组+链表+红黑树的结构可能不一定比数组+链表的结构性能高。就好比杀鸡焉用牛刀的意思。
(2)HashMap频繁的扩容,会造成底部红黑树不断的进行拆分和重组,这是非常耗时的。因此,也就是链表长度比较长的时候转变成红黑树才会显著提高效率。
存储元素
大多使用下面的方式存储一个元素。
public class Test {
public static void main(String[] args) {
HashMap<String, Integer> map= new HashMap<>();
//存储一个元素
map.put("张三", 20);
}
}
HashMap<String, Integer>,第一个参数是键,第二个参数是值,合起来叫做键值对。存储时只需要用put方法即可。底层实现原理流程图如下:

(1)第一步:调用put方法传入键值对。
(2)第二步:使用hash算法计算hash值。
(3)第三步:根据hash值确定存放的位置,判断是否和其他键值对位置发生了冲突。
(4)第四步:若没有发生冲突,直接存放在数组中即可。
(5)第五步:若发生了冲突,还要判断此时的数据结构是什么?。
(6)第六步:若此时的数据结构是红黑树,那就直接插入红黑树中。
(7)第七步:若此时的数据结构是链表,判断插入之后是否大于等于8。
(8)第八步:插入之后大于8了,就要先调整为红黑树,在插入。
(9)第九步:插入之后不大于8,那么就直接插入到链表尾部即可。
上面是插入数据的整个流程,下面深入到源码中去看看底层实现。查看put方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
通过源码可以看到,put方法其实调用的是putVal方法。putVal方法有5个参数:
(1)第一个参数hash:调用了hash方法计算hash值。
(2)第二个参数key:就是我们传入的key值,也就是例子中的张三。
(3)第三个参数value:就是我们传入的value值,也就是例子中的20。
(4)第四个参数onlyIfAbsent:也就是当键相同时,不修改已存在的值。
(5)第五个参数evict :如果为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;
}
此段代码要结合上一开始画的流程图进行拆分分析(整体分了四大部分): Node<K,V>[] tab中tab表示的就是数组。Node<K,V> p中p表示的就是当前插入的节点
第一部分
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
这一部分表示如果数组是空的,那么就通过resize方法来创建一个新的数组。在这里resize方法先不说明,在下一小节扩容时会提到。
第二部分
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
i表示在数组中插入的位置,计算的方式为(n - 1) & hash。在这里需要判断插入的位置是否是冲突的,如果不冲突就直接newNode,插入到数组中即可,这就和流程图中第一个判断框对应了。如果插入的hash值冲突了,那就转到第三部分,处理冲突。
第三部分
else {
Node<K,V> e; K k;
//第三部分a
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//第三部分b
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//第三部分c
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;
}
}
//第三部分d
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
整个处理冲突有点繁琐,所以进行了一些划分。
第一小节
if (p.hash == hash
&&((k = p.key) == key || (key != null && key.equals(k))))
e = p;
在这里判断table[i]中的元素是否与插入的key一样,若相同那就直接使用插入的值p替换掉旧的值e。
第二小节
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
判断插入的数据结构是红黑树还是链表,在这里表示如果是红黑树,那就直接putTreeVal到红黑树中。这和流程图里面的第二个判断框对应。
第三小节
//第三部分c
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;
}
}
如果数据结构是链表,首先要遍历table数组是否存在,如果不存在直接newNode(hash, key, value, null)。如果存在了直接使用新的value替换掉旧的。
注意一点:不存在并且在链表末尾插入元素的时候,会判断binCount >= TREEIFY_THRESHOLD - 1。也就是判断当前链表的长度是否大于阈值8,如果大于那就会把当前链表转变成红黑树,方法是treeifyBin。和流程图中第三个判断框对应。
第四部分
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
插入成功之后,还要判断一下实际存在的键值对数量size是否大于阈值threshold。如果大于那就开始扩容了。
扩容
扩容流程图

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 {
//如果是多个节点的链表,将原链表拆分为两个链表
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);
//链表1存于原索引
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//链表2存于原索引加上原hash桶长度的偏移量
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
下面开始分段分析
第一部分
//第一部分:扩容
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
}
如果超过了数组的最大容量,那么就直接将阈值设置为整数最大值(2的30次方(1左移30)),如果没有超过,那就扩容为原来的2倍,这里要是oldThr << 1,移位操作来实现的。HashMap的最大容量/阈值是2的30次方(1左移30):
static final int MAXIMUM_CAPACITY = 1 << 30;
第二部分
//第二部分:设置阈值
else if (oldThr > 0) //阈值已经初始化了,就直接使用
newCap = oldThr;
else { // 没有初始化阈值那就初始化一个默认的容量和阈值
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;
首先第一个else if表示如果阈值已经初始化过了,那就直接使用旧的阈值。然后第二个else表示如果没有初始化,那就初始化一个新的数组容量和新的阈值。
第三部分
//第三部分:旧数据保存在新数组里面
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);
//链表1存于原索引
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//链表2存于原索引加上原hash桶长度的偏移量
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
第三部分同样也很复杂,就是把旧数据复制到新数组里面。需要注意几种情况:
A:扩容后,若hash值新增参与运算的位=0,那么元素在扩容后的位置=原始位置。
B:扩容后,若hash值新增参与运算的位=1,那么元素在扩容后的位置=原始位置+扩容后的旧位置。
hash值新增参与运算的位是把hash值转变成二进制数字,新增参与运算的位就是倒数第五位。扩容后长度为原hash表的2倍,于是把hash表分为两半,分为低位和高位,原链表的键值对,一半放在低位,一半放在高位,且通过e.hash & oldCap == 0来判断。举个例子:n = 16,二进制为10000,倒数第5位为1,e.hash & oldCap是否等于0就取决于e.hash倒数第5位是0还是1,这就相当于有50%的概率放在新hash表低位,50%的概率放在新hash表高位。还有一个问题,扩容之后出现了地址冲突如何解决。
解决地址冲突
地址冲突的前提是计算的hash值出现了重复,先来看看JDK1.8中HashMap中如何计算hash值的。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
JDK1.7
static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
return h & (length-1); //第三步 取模运算
}
代码是超级简单,hash值其实就是通过hashcode与16异或计算的来的,为什么要使用异或运算呢?请看下图:

一开始开辟一个长度为16的空间的数组;
将要存储的对象转换成整数,用16取模,结果会对应到数组中的一个索引;
如果发生哈希冲突,取模的结果会指向数组中同一个索引;
将产生哈希冲突的对象链入其对应索引挂接的对象的后面,形成一条链表;
数组中的每个索引后都挂有一条链表,这就是所谓的链地址法(Separate Chain);
注意:当链表长度大于8且数组长度大于64时,Java会将其转换以红黑树。
构造HashMap
HashMap的构造方法一共有四个:
第一个:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
第二个:
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
第三个:
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
第四个:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
这四个构造方法中第四个最麻烦,只要能弄懂第四个构造方法,其他三个自然而然也就明白了。上面出现了两个新的名词:loadFactor和initialCapacity。
(1)initialCapacity初始容量
官方要求我们要输入一个2的N次幂的值,比如说2、4、8、16等等但,如果输入了一个20,虚拟机会根据你输入的值,找一个离20最近的2的N次幂的值,比如说16离它最近,就取16为初始容量。
(2)loadFactor负载因子
负载因子,默认值是0.75。负载因子表示一个散列表的空间的使用程度,有这样一个公式:initailCapacity*loadFactor=HashMap的容量。 所以负载因子越大则散列表的装填程度越高,也就是能容纳更多的元素,元素多了,链表大了,所以此时索引效率就会降低。反之,负载因子越小则链表中的数据量就越稀疏,此时会对空间造成烂费,但是此时索引效率高。
为什么默认值会是0.75呢?我们截取一段jdk文档:
Ideally, under random hashCodes, the frequency of
nodes in bins follows a Poisson distribution
(http://en.wikipedia.org/wiki/Poisson_distribution) with a
parameter of about 0.5 on average for the default resizing
threshold of 0.75, although with a large variance because of
resizing granularity. Ignoring variance, the expected
occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
factorial(k)). The first values are:
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
more: less than 1 in ten million
由于TreeNode的大小是普通节点(Node)的两倍,因此只有当 bin 中包含足够多(即树化的阈值 TREEIFY_THRESHOLD)的节点时才会转为TreeNode;而当bin中节点减少时(删除节点或扩容),又会把红黑树再转为链表。
看第三行Poisson_distribution泊淞分布嘛。当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为加载因子,每个碰撞位置的链表长度超过8个是几乎不可能的,概率(0.00000006)不足千万分之一。当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为加载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。