HashMap1.8源码解析
首先看一下HashMap1.8的继承关系:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {}
和1.7一样1.8不仅继承了AbstractMap,而且实现了Map、Cloneable和Serializable接口,所以HashMap也可以序列化。
HashMap1.8的存储结构:
在1.7中,HashMap是以“数组+链表”的基本结构来存储key和value构成的Entry单元的。其中链表结构的存在是用来处理hash碰撞的。这种结构有它的优点,比如容易实现等。但是我们可以设想这样一种情况,如果说有成百上千个节点在hash时发生碰撞,存储一个链表中,那么如果要查找其中一个节点,那将不可避免的花费o(n)o(n)的时间复杂度来进行查找。基于这点,1.8中将HashMap的基本结构进行了改善,其中hashMap的基本结构依然是“数组+链表”,但是当hash碰撞太多以至于链表过长的时候,链表结构将演化成树(具体来说应该是红黑树)的结构。我们都知道,红黑树是二叉查找树平衡形式的一种,因此查找性能较链表来说,有了很大提升。
在1.7中,是使用Entry这个类作为基本存储单元的,在1.8中,可能为了配合红黑树的使用,改进成了Node这个类,当然,差不多只是名字变了而已,类内部实现的形式差别不是很大。
可以通过下面这张图理解:
HashMap1.8的成员属性:
private static final long serialVersionUID = 362498820763181265L;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16,最小容量:16
static final int MAXIMUM_CAPACITY = 1 << 30;//HashMap的最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认的负载因子,这里和1.7中的原因是一样的,都是为了在时间和空间上做一个折中,选择最合适的负载因子以保证最优化
//树的门阀值,即当链表的长度超过这个值的时候,进行链表到树结构的转变
static final int TREEIFY_THRESHOLD = 8;
//当低于这个值时,树变成链表
static final int UNTREEIFY_THRESHOLD = 6;
//下面这个值的意义是:位桶(bin)处的数据要采用红黑树结构进行存储时,整个Table的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
//分配的时候,table的长度总是2的幂
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
//总的KV数量
transient int size;
//这个值用于快速失败机制
transient int modCount;
//门限阈值,计算方法:容量*负载因子
int threshold;
HashMap1.8的构造方法:
// 初始容量和加载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
// 如果传入的数组的大小小于0,抛出异常
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 如果传入的数组的大小最大值就是MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 加载因子比0小,或者加载因子不是一个number,抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
// 只传输一个数组的大小,加载因子是默认的0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 无参构造,加载因子是默认的0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 传递一个Map类型的m,加载因子还是默认的0.75
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
HashMap的put方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
put方法直接调用了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;
// 现在的tab表示Node数组,n表示其长度
if ((p = tab[i = (n - 1) & hash]) == null)
// 执行到这里,表明当前的索引到的index下标,还未存放元素
tab[i] = newNode(hash, key, value, null);
else {
// 执行到这里,表明当前index下标已经存放了元素
Node<K,V> e; K k;
// 检测要放的元素的key和已经存放在index下标的元素的key是否相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 这里表明两个key是相等的,那么进行覆盖,替换原来的旧值
e = p;
else if (p instanceof TreeNode) // 若两个key不相等,那么检测当前下标所形成的非线性结构是否是红黑树?
// 执行到这里,表明非线性结构是红黑树
// 那就把它插入到红黑树里面
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 执行到这里,表明存储结构是链表
// 那就先对链表进行遍历,检测是否存过在这个key
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 // 这里进行判断,是否当前链表的长度已经 >= 8
// 执行到这里,表明已经 >= 8,那么将这个链表转化成红黑树进行存储
// 当然,我们前面提到,转化成红黑树需要两个条件,这里只满足了其中之一
// 第二个条件在treeifyBin()函数里面进行判断
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) // 这里判断是否存在key相等的节点
// 相等,跳出循环
break;
p = e;
}
}
// 由于e!=null,那么就说明存在key,
if (e != null) { // existing mapping for key
// 这里进行旧值的替换
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 开始执行子类覆盖节点之后的方法
afterNodeAccess(e);
// 因为是覆盖,所以长度并未增加,可以直接返回
return oldValue;
}
}
// 这个modCount是作为迭代器的总长度来用,
++modCount;
// 先对hashmap的总容量进行+1,然后比较它和阈值的大小
if (++size > threshold)
// 已经比阈值大,那么进行扩容
resize();
afterNodeInsertion(evict);
return null;
}
在putVal方法中有这样一条判断:
if ((p = tab[i = (n - 1) & hash]) == null)
实际上是hash值对(数字长度-1)取余(这里用位运算是因为位运算效率高)。
table的下标是通过将(数组长度-1)与hash值进行与运算得到的。在这个下标对应的位置进行存储。
得到这个下标,进行了一系列操作。目的是为了将它进行位扰动,从而增加散列度,减少哈希碰撞。
进行插入操作,分为三种情况:
1.插入位置无数据,直接存入
2.插入位置有数据,但是较少且符合链表结构存储的条件,那么以链表操作存入
3.插入位置有数据,但是以树结构进行存储,那么以树的相关操作进行存入
较1.7的put相比,复杂了很多,不过却换取了查找时的性能提升。
注:
HashMap真正的初始化还是在put方法中进行的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;
// 判断table是否已经初始化过
if (oldCap > 0) {
// oldCap > 0,说明table已经初始化过了
// 判断旧的数组的容量是否已经达到或多于默认的最大容量(1 << 30)
if (oldCap >= MAXIMUM_CAPACITY) {
// 改变阈值为整型量的最大值:
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 左移1位,表示扩大到原来的两倍
newThr = oldThr << 1; // double threshold
}
// 判断旧的数组的阈值是否大于0
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;
// 如果当前的数组元素不为null,把值赋值给e
if ((e = oldTab[j]) != null) {
// 把当前的数组元素赋值为null
oldTab[j] = null;
if (e.next == null)
// 表明数组元素没后后继节点,该桶中只有一个节点
// 将该节点放到新数组中(下标通过hash运算和长度-1相与得到)
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;
// 判断当前的bin在新数组中是否改变了位置
if ((e.hash & oldCap) == 0) {
// 结果为0,表明在新数组中的位置没有变动
if (loTail == null)
// 当前的低位的头指向e所指向的空间,也就是链表的头部
loHead = e;
else
// 当前低位的尾的next指向e所指向的空间
loTail.next = e;
// 当前的低位的尾指向了e所指向的空间
loTail = e;
// 上述这几条语句,目的就是为了让头指针指向链表的头部,尾指针一直指向e所指向的空间
}
else {
// 这个else里面的语句的意思与上面if里面的意思一样,只不过是这个是存放在数组中下标高的链
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);// 这个do-while循环,目的就是进行链的遍历,并自行判断应该放在原来的位置还是新的位置。
// 然后下面这些语句是将这条链放在新的数组中,
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
// 执行完一次,继续循环执行,直到循环完旧的数组
}
}
// 将新生成的数组进行返回,这也是等于是将旧的数组进行了扩容
return newTab;
}
树化操作treefyBin:
//对链表进行树结构的转化存储
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
关于HashMap中的tableSizeFor方法
//返回根据给定的目标容量所计算出来的最接近的2的幂,这有利于改善hash算法
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
总结:
1.8的HashMap源码较多,这里挑了一些讲下,其余的还是很好理解的。
- 1.8中HashMap的基本结构还是以数组+链表的形式来存储的,链表没有达到树化的最小数量MIN_TREEIFY_CAPACITY,则进行扩容操作。满足树化的条件,则把链表的每个节点都转化为 TreeNode。通过TreeNode的treeify(Node<K,V>[] tab)方法构建树。
- 源码要求HashMap底层实现数组的长度为2的幂,原因是可以得到较好的散列性能。