HashMap
是我们日常开发中处理键值对最常用的数据结构。JDK1.8对HashMap的底层实现进行了优化,如引入了红黑树、resize()调整、优化了高位运算的hash算法等。
由于JDK1.8中引入了红黑树,这也成为理解HashMap的重要一环,感兴趣的同学可以阅读我之前写的一篇文章:TreeMap源码分析(红黑树的实现过程)。本文不再分析红黑树的新增删除过程(比较复杂,尤其删除节点过程)
在JDK1.8之前,HashMap采用了数组+链表实现
若hash碰撞较多,链表的长度可能过长,严重影响了HashMap的性能。JDK1.8之后,引入了红黑树处理链表过长的情况,将复杂度由O(N)优化为O(logN)
特性: HashMap根据key的hashCode值存储数据,具有很快的访问速度,遍历HashMap的顺序是不确定的。HashMap最多允许一个null
键,允许多个null
值。HashMap非线程安全,在高并发情况下操作同一个HashMap可能会导致数据丢失等问题,建议采用ConcurrentHashMap
桶数组:transient Node
一个hash表数组,用来存放Node
或者TreeNode
Node:HashMap的内部类,单向链表,实现了Map.Entry接口,存放键、值、hash以及下一个节点的引用
TreeNode:JDK1.8中新引入的红黑树节点,不详述,具体参考TreeMap源码分析(红黑树的实现过程)
static class Node<K,V> implements Map.Entry<K,V> {
//key的hashCode,用来定位桶的索引
final int hash;
final K key;
V value;
//指向下一个Node节点
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//父节点
TreeNode<K,V> parent; // red-black tree links
//左孩子
TreeNode<K,V> left;
//右孩子
TreeNode<K,V> right;
//上一个节点的引用,和next一起用于保留Node的顺序
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
......
}
通过以上3个数据结构,结合前面的原理图,我们就可以看出HashMap基本的实现过程了。
有一个桶数组,添加键值对元素的时候我们根据key计算hash值,通过和桶数组长度的模运算确定该元素在数组中添加的位置,但该位置可能已经存在相同hash的元素了,这时候就将其插入到该位置最后一个元素的后面,这样就形成了一条链表。若该链表长度超过一定值时,我们将该链表转化为红黑树,从而提升查询效率
这就是JDK1.8中HashMap的最基本原理,我们后面将详细分析。
threshold
:当前HashMap所能容纳的最大Node数量,超过该数量需要扩容(resize)。该值是由capacity * loadFactor计算出的
loadFactor
:负载因子。相同的数组长度,负载因子越大,可容纳的Node越多。通过调节负载因子的大小我们可以调节时间与空间的利用效率。默认值0.75是比较平衡的一个选择,一般情况下不需要改变
modCount
:用于记录HashMap内部结构发生变化的次数,用于迭代的快速失败(fail-fast)。在迭代开始时,会将modCount赋给expectedModCount,迭代过程中,会对2个值进行比较,若不相等(modCount变化了),则会抛出ConcurrentModificationException异常
size
:HashMap中存储的Node数量
一些常量:
//默认的桶数组初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当链表上Node数量超过8时转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
//当红黑树上TreeNode数量小于6时转化为链表
static final int UNTREEIFY_THRESHOLD = 6;
//当桶数组大小大于64时才考虑转化为红黑树(即优先对table进行扩容)
static final int MIN_TREEIFY_CAPACITY = 64;
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);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
HashMap一共有4个构造方法,我们最常用的是无参构造方法,其中将loadFactor设为默认值。
第一个构造方法中我们会发现有几个奇怪的地方:①、参数中的initialCapacity初始容量只参与到了计算threshold的过程,而没有定义桶数组的初始容量。②、之前说过threshold=capacity * loadFactor,但这里并不是这样计算的,而是通过tableSizeFor计算得到的。 这两个问题我们在后面的扩容机制中会进行说明,我们先来看一下tableSizeFor这个方法是做什么的。
//返回大于或等于cap的最小2的幂
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;
}
直接看上面的计算过程,我想一般人都是很难理解的,我们可以自己手动计算一下。如我们指定cap=15,按照上面的步骤最终计算得到的结果就是16
//计算key的hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//定位元素在桶数组中的位置的算法,其实就是取模
tab[(n - 1) & hash]
总的来说,JDK1.8中的hash算法分为以下三步:
①取key的hashCode值
②对hashCode做高位运算
③和桶数组长度取模确定元素位置
我们先来看下为什么 (n - 1) & hash 是取模运算:
举个例子,hash值为30,桶数组大小n为16,30%16=14,我们再用位运算计算一下(只标注了1个字节,省略左边的0)
这是一个很巧妙的设计,因为位运算的效率是大于模运算的
再看下为什么需要做高位运算:因为我们通常声明HashMap的时候不会指定桶数组大小或者说不会声明很大的值,这时候若直接取key的hashCode与n-1做位与运算(也就是取模,跳过第②步),由于n-1的数值较小,那么二进制高位都为0,而hashCode一般数值较大,二进制高位是有1的,位与后高位仍未0,最终实际上参与运算的只有低位。因此将key的hashCode向右移16位后再与自身做异或运算(int是4字节32位,前16位为高位,后16位为低位),使得高位也参与了运算,增加了hash的复杂度,减少了碰撞概率
举个例子整体看一下hash算法:对"test"这个String做hash算法
public V get(Object key) {
Node<K,V> e;
//hash算法前文已详述
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//tab[(n - 1) & hash]定位出元素所在的位置,前文已详述。这里取出第一个Node节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//和首节点比较,若匹配则直接返回第一个Node节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//若首节点有后续节点
if ((e = first.next) != null) {
//若首节点是红黑树TreeNode节点,则调用getTreeNode查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//对该条链表循环查找,返回匹配节点
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
查找方法中的hash算法前文已详述,其它过程通过注释可以轻松理解,红黑树查找过程不做详述
HashMap在遍历时的顺序和元素插入的顺序一般都是不一样的
我们以HashMap的keySet举例:
for (Object key : map.keySet()) {
//TODO
}
这种遍历在编译时等价于通过迭代器遍历:
Set keys = map.keySet();
Iterator iterator = keys.iterator();
while (iterator.hasNext()) {
Object key = iterator.next();
//TODO
}
以下是遍历相关源码
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
//index指向第一个包含节点的桶位置
//next指向第一个桶中的第一个节点
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
//前面提到的modCount比对,用于fail-fast
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
//next指向下一个Node,若为null则寻找下一个包含Node的桶
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
对上面的HashMap遍历key做一个总结:首先获取HashMap的KeySet
集合对象,再获取KeySet的迭代器KeyIterator
。KeyIterator继承自HashIterator
,在HashIterator初始化构造方法中会从桶数组中找到第一个包含Node的桶。在之后的nextNode()
方法中会遍历该桶中的链表,遍历结束后会寻找下一个包含Node的桶,之后重复以上过程。
我们发现遍历桶中的Node只是不停的去寻找Node的next,并没有区分是链表还是红黑树,这是因为红黑树中仍然保留了next的引用
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//table为空则进行初始化,HashMap在第一次插入数据时才进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//(n - 1) & hash为上文说的取模运算计算位置,若桶中该位置不包含任何Node,则将新节点直接放入桶中即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//若该位置的桶中已经存放了Node节点
else {
Node<K,V> e; K k;
//新元素和桶中第一个Node做比较,若key的值以及hash的值均相等,则将e指向第一个Node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//若该Node为红黑树则调用红黑树的插入方法(红黑树插入过程不详细阐述)
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//若该链为链表
else {
//对链表进行遍历
for (int binCount = 0; ; ++binCount) {
//遍历到了该链表的尾部(最后一个Node的next肯定是null)
if ((e = p.next) == null) {
//此时将新Node插入到尾部(作为之前最后一个Node的next引用)
p.next = newNode(hash, key, value, null);
//若链表长度大于或等于树化阈值,转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
//跳出循环
break;
}
//若遍历过程中发现存在key的值以及hash的值均相等,则直接跳出循环,此时e不为null
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//将p指向e继续遍历
p = e;
}
}
//若链表中存在key值以及hash值均相等的节点
if (e != null) { // existing mapping for key
//记录e的value值
V oldValue = e.value;
//若onlyIfAbsent为false或者旧值为null(onlyIfAbsent若为true,表示不替换已有的value)
if (!onlyIfAbsent || oldValue == null)
//用新Node的值替换旧Node的值
e.value = value;
//用于LinkedHashMap回调,HashMap不处理
afterNodeAccess(e);
//返回旧值
return oldValue;
}
}
//修改次数+1
++modCount;
//若桶中Node数量超过阈值则扩容
if (++size > threshold)
resize();
//用于LinkedHashMap回调,HashMap不处理
afterNodeInsertion(evict);
return null;
}
resize
是HashMap最核心的一部分,由于桶数组的长度是有限的,若不扩容数组长度,那么随着元素的新增,碰撞概率会越来越大,极大的降低了效率。扩容的时机是初始化或元素个数超过阈值(桶数组长度*负载因子),每次扩容后桶数组长度扩大1倍,阈值也扩大一倍。之后重新计算元素的位置并调整
final Node<K,V>[] resize() {
//定义oldTab、oldCap、oldThr、newCap、newThr
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//oldCap大于0,说明桶数组不为空,即已经初始化过了
if (oldCap > 0) {
//若桶数组长度大于等于最大值,则将阈值赋为整数最大值并停止扩容,返回原桶数组
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//若oldCap扩容1倍后小于最大值且oldCap大于等于初始容量16,则将oldThr也扩容1倍并赋给newThr
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//oldThr大于0说明走的是有参的构造方法,这里将oldThr赋给newCap
//这里可能会感觉很奇怪,为什么会把阈值赋给桶数组长度,我们后面具体分析
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//走的无参的构造方法,newCap为默认的桶数组容量16,newThr为默认的负载因子*默认的桶数组容量=0.75*16=12
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//newThr为0时,按照阈值公式计算阈值大小
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将新的阈值赋给threshold
threshold = newThr;
//创建新的桶数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//table指向新的桶数组
table = newTab;
//若旧的桶数组不为空
if (oldTab != null) {
//遍历桶数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//e指向oldTab[j],若不为空
if ((e = oldTab[j]) != null) {
//将oldTabl[j]置空,便于之后gc
oldTab[j] = null;
//若e.next为null,也就是说旧的桶数组在j位置只有1个Node节点e
//那么对新的桶数组取模计算出存放位置并存放旧的Node节点e
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//旧桶数组该位置不止1个Node节点且e为TreeNode红黑树,则对红黑树进行拆分,这里之后会具体分析拆分过程
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//旧桶数组该位置不止1个Node节点,且为Node链表
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//遍历链表,进行分组
do {
//next指向下一个节点
next = e.next;
//若e的hash值与旧桶数组长度位与后等于0
if ((e.hash & oldCap) == 0) {
//若loTail为null,则将loHead指向e
if (loTail == null)
loHead = e;
//否则将loTail.next指向e
else
loTail.next = e;
//loTail指向e,做下一次循环
loTail = e;
}
//若e的hash值与旧桶数组长度位与后不等于0,同上
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
//一直循环到该链表尾部
} while ((e = next) != null);
//若loTail不为null
if (loTail != null) {
//loTail.next置为null
loTail.next = null;
//将loHead赋给新的桶数组的j位置
newTab[j] = loHead;
}
//若hiTail不为null
if (hiTail != null) {
//hiTail.next置为null
hiTail.next = null;
//将hiHead赋给新的桶数组的j+oldCap位置
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回新的桶数组
return newTab;
}
结合注释我们分析一下扩容的流程:
计算新数组的长度、阈值会分为几种情况,这在上面代码已经通过注释说明,大家可以自行理解,这里我们分析其中一种情况:oldCap=0 && oldThr>0。这是之前创建HashMap时走的有参的构造方法,相等于newCap = oldThr = tableSizeFor(initialCapacity)
。也就是说我们调用有参的构造方法时传入的初始长度值会经过tableSizeFor方法变成大于或等于它的最小2的幂存入threshold
,之后第一次插入元素初始化时再赋给newCap,最后再通过阈值公式重新计算newThr。
总结:threshold只是临时存储了经过二次计算(保证长度是2的幂)的桶数组长度,最终把这个值赋给了新的桶数组长度,新桶的阈值还是通过阈值公式计算的 这里就解答了前面分析构造方法时抛出的2个问题
遍历旧桶数组,重新分配到新桶数组中。在遍历过程中有3种情况:1、桶(指遍历桶数组的某一个位置)里只有1个Node 2、桶里是TreeNode红黑树 3、桶里是链表
第一种很简单,对新的桶数组取模计算出存放位置并存放旧的Node节点。第二种需要做红黑树拆分,这个放到后面和链表树化一起看。第三种对链表做分组,再分配到新桶对应的位置
在JDK1.7中,resize链表是循环遍历桶数组,元素的hash对新桶数组长度取模后定位新的位置并插入(而且有可能需要重新hash)。而在JDK1.8中,resize链表做了优化,也就是前面说的分组
以桶数组大小为16举例,有2个元素,hash值为23、7,由于n-1=1111,只有后四位参与位与运算,所以得到的结果都是相同的,也就是这2个元素都存储在7号桶中,形成一条Node链表
我们将这个大小16的桶数组扩容1倍,即32,再对这2个元素取模确定在新桶中的位置
扩容的时候把桶数组容量扩大1倍,那么n-1就会在高位多出1位。比如16扩容到32,n就是0001 0000
->0010 0000
,n-1就是0000 1111
->0001 1111
。也就是说参与运算的bit个数由4个变成5个,那么多出的1位bit就有可能影响运算结果,而这位bit就是扩容前的n二进制1所占的位置(n=16=0001 0000
第五位)。若hash在该位置为0,则位与后计算结果不变,该元素仍处于原位置;若hash在该位置为1,则位与后计算结果改变,该元素处于原位置+原桶数组长度。
所以,通过e.hash & oldCap
就可以得知hash在oldCap二进制里1对应的位置是0还是1。说的可能有点抽象,结合上面2张图能更好的理解
根据e.hash & oldCap
的结果将链表分组,为0的放在lo链表,为1的放在hi链表,最后把lo链表放在新数组原位置,hi链表放在新数组原位置+原桶数组长度的位置,这样就完成了这条链表的重分配。
对比JDK1.7,JDK1.8省去了重新计算hash的时间,并且由于扩容后hash所对应新增的1bit位置0、1是随机的,这样还可以把这条链表中的Node均匀分布到新的桶数组中。
开头已经列出TreeNode源码以及一些相关常量,红黑树内部的转化过程这里不细说,可以参考TreeMap源码分析(红黑树的实现过程),我们这里看一下树化和拆分
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//若桶数组长度小于树化阈值64,优先扩容数组长度而非转化为红黑树
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
//定义头结点hd,尾节点tl
TreeNode<K,V> hd = null, tl = null;
//遍历Node链表
do {
//将Node节点转化为TreeNode节点
TreeNode<K,V> p = replacementTreeNode(e, null);
//若尾节点tl为null(第一次遍历),将头结点hd指向创建的TreeNode节点
if (tl == null)
hd = p;
//否则将p.prev指向tl节点,并且将tl.next指向p节点(将2个TreeNode节点通过next、prev关联起来,保持了顺序)
else {
p.prev = tl;
tl.next = p;
}
//将tl指向p节点,进行下一次循环
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
//将树形链表转化为红黑树,涉及到转化红黑树过程省略,可参考前面提到的文章
hd.treeify(tab);
}
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
//不详述
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
我们发现树化不仅仅要满足之前说的Node链表长度大于等于阈值8,同时还要满足桶数组的容量大于等于阈值64。因为桶数组容量过小时元素的hash碰撞概率很高,与其树化链表不如扩容桶数组的容量。同时桶数组容量较小时扩容操作会比较频繁,扩容就会再次拆分红黑树,耗时耗力。
通过树化,链表不仅转化为了红黑树,同时也保留了原链表的顺序(next、prev引用),但红黑树的root节点会移动到链表首位,方便一些操作
如果之前接触过TreeMap源码那就会知道红黑树在实现的时候要求键key必须实现Comparable
接口,只有这样TreeMap
才知道如何比较2个值的大小。但HashMap在设计之初是没有考虑到这点的,我们从treeify
方法里看看是如何做的:
Comparable
接口,且2个比较元素的key是相同的Class,那么通过compareTo方法比较大小,相同继续判断tieBreakOrder
,其实就是调用System.identityHashCode
比较大小下面看一下拆分红黑树:
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
//由于TreeNode保留了链表的next引用,所以和之前resize里遍历链表分组的方式是一样的
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
由于TreeNode在之前树化的过程中保留了Node的特性(next引用),所以我们在扩容拆分红黑树时,完全可以按照之前拆分Node链表一样的方式进行分组后重新塞入新桶数组。不同之处在于将红黑树拆分为lo和hi两条链表后,若链表长度小于等于阈值6,才将该链表转化为Node链表,否则将该链表重新树化
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//获得元素所在桶数组的位置index以及第一个Node-p,若p不存在则直接返回
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//删除元素和桶中第一个Node做比较,若key的值以及hash的值均相等,则将node指向第一个Node-p
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//若桶数组该位置不止1个元素
else if ((e = p.next) != null) {
//若p是红黑树,则调用红黑树查找节点方法,不细说
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
//若是普通链表
else {
//循环该链表找到待删除Node
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//调用红黑树的删除方法,不细说
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//若node是链表的第一个节点,则将该位置第一个节点替换为下一个节点(相当于删除第一个节点)
else if (node == p)
tab[index] = node.next;
//若node不是链表第一个节点,则将p.next指向node.next,相当于删除node节点
else
p.next = node.next;
//修改次数+1
++modCount;
//实际大小-1
--size;
afterNodeRemoval(node);
//返回被删除的Node
return node;
}
}
return null;
}
删除很简单了,看下注释就好了
(h = key.hashCode()) ^ (h >>> 16)
,让高位参与了hash运算,增加了hash的复杂度,减少了碰撞概率感谢有耐心的你阅读到这里,JDK1.8 HashMap就介绍完了,和JDK1.7对比起来,优化点确实很多,源码很值得学习。本文没有细说红黑树的查找插入删除过程,有兴趣的可以参考我之前的一篇文章TreeMap源码分析(红黑树的实现过程)
美团Java 8系列之重新认识HashMap
JDK1.8 HashMap源码分析
HashMap 源码详细分析(JDK1.8) 非常棒的一篇文章,强烈推荐