最近在研究Java集合的内容,本来想像前面那样将整个集合都放在一篇博客里面,但发现HashMap、ConcurrentHashMap还有LinkedConcurrentHashMap里面的知识太多太多了,而且拜读了源码以后,才真正地感受到了集合设计者的厉害,所以还是把这三个集合单独列出来写博客来理解吧。在看的过程中,看了很多博客,感觉有些东西其他博客还是没有讲清楚,这里我将我遇到的每个问题都详细的查了,在这里做一个记录,希望能帮助到有需要的同学。
红黑树的性质
红黑树的插入过程:
红黑树的插入遵循以下规则:
1.如果根节点为空,直接插入,并设置根节点为黑色
2.根节点不为空,将新插入的节点cur标记为红色。如果cur的父节点不为黑色,在则需要分情况讨论
1.常见的几种哈希函数
2.冲突的解决
①开发地址法(再散列法): 主要有以下几种再散列的方式:
②链地址法: HashMap的实现方式,用数组当桶,发生冲突时,在桶中以链表链接
在JDK1.8中,HashMap使用链地址法的方式来实现,其先用一个数组作为桶,然后桶中存放的要么是链表(链表的节点个数小于等于8),要么是红黑树,要么为空。整个数据结构如下所示:
HashMap每次扩容,容量都是2的幂次方。下面的方法是获得大于cap且最近的2的整数次幂的数。如输入10,则返回16。其中cap-1
的的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。显然不是结果。减1后二进制为111,再进行操作则会得到原来的数值1000,即8。
举个栗子,如果传入参数10,那计算的过程如下:
举个栗子:
这样做的好处是,可以将hashcode高位和低位的值进行混合做异或运算,而且混合后,低位的信息中加入了高位的信息,这样高位的信息被变相的保留了下来。掺杂的元素多了,那么生成的hash值的随机性会增大。
知道hash值后,根据hash值来计算其在数组中的位置,计算公式为:
pos = (n - 1) & hash
(非常重要),其中n为数组的长度。由于null对象不能计算hash值,所以null对象都放在桶中的固定位置,HashMap中使用第0个桶来存放键为null的键值对。
先根据key
的hashCode
计算得到hash值,然后利用pos = (n - 1) & hash
计算其在数组中的位置,并在该位置中看,该位置存的是链表还是红黑树,然后再在链表/红黑树中进行查找。
主要通过putVal()
函数实现,putVal
的过程中,如果没有达到链表的阈值长度(默认是8),则直接加入到链表的尾部。如果链表长度超过8,则将链表变化为红黑树。代码如下,必要的地方都做了详细的注释:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node<K,V>[] tab; HashMap.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 {
HashMap.Node<K,V> e; K k;
if (p.hash == hash && //这种情况下,说明放入了重复元素,需要根据key更新value
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof HashMap.TreeNode)//这种情况下,在红黑树中插入节点
e = ((HashMap.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) //这里链表长度大于阈值,需要将链表转为红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&//链表中已经存在key了,需要根据key更新value
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // 链表中存在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;
}
通过resize函数将数组扩容为原来容量的2倍后,需要重新将原有的元素映射到数组中,映射时,分以下三种情况:
如果原来的位置只有一个节点,直接通过上面的pos = (n - 1) & hash
计算其在数组中 的位置。
如果是链表,进行链表的rehash时,根据hash & oldCap
的结果是0还是1,将链表拆分成两部分。
举个栗子(栗子来自于别人的博客,我找不到出处了):如果原数组的容量为16,那n-1=15
,然后有两个Entry,第一个Entry的key的hashCode值为1111 1111 1111 1111 0000 1111 0000 0101
,第二个Entry的key的hash值为1111 1111 1111 1111 0000 1111 0001 0101
,那在扩容之前,通过pos = (n - 1) & hash
这个公式计算得到,他们在桶中的位置索引都是5(二进制为00101)
,当扩容后,数组长度变成原来的两倍即32,那n-1=31
,用二进制表示就是1 1111
,然后通过pos = (n - 1) & hash
这个公式,就能把上面两个节点分成两个部分。具体的看第二张图的红字部分。
如果是红黑树,就调用红黑树的split()
方法,此处,会对红黑树也和链表一样分为高位和低位两个部分,如果树中元素的个数小于6个了,就将红黑树转换成链表。
扩容过程的代码如下:
final HashMap.Node<K,V>[] resize() {
HashMap.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; // 新的threshold也变成原来的2倍(装载因子没变,容量扩大二倍,阈值自然也扩大二倍)
}
else if (oldThr > 0) //容量在threshold之内
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"})
HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];//开辟新的数组空间
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
HashMap.Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)//原来桶中只有一个元素,直接利用pos = (n - 1) & hash的公式重新计算在数组中的位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof HashMap.TreeNode)//原来桶中是红黑树,分裂红黑树
((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 原来桶中是链表,进行链表的rehash
HashMap.Node<K,V> loHead = null, loTail = null;
HashMap.Node<K,V> hiHead = null, hiTail = null;
HashMap.Node<K,V> next;
// 将同一桶中的元素根据(e.hash & oldCap)是否为0进行分割,分成两个不同的链表,完成rehash
do {
next = e.next;
//(e.hash & oldCap) == 0分为一个链表部分
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//(e.hash & oldCap) == 1分为一个链表部分
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) {//在数组中的位置后移oldCap个位置
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
什么时候扩容: 当前容器中元素的个数(是HashMap中的size,而不是使用桶的个数)超过阈值(当前数组长度乘以加载因子的值)的时候,就要自动扩容。
其中,上面说的链表的方法很好理解,再来看看红黑树的split
方法:
final void split(HashMap<K,V> map, HashMap.Node<K,V>[] tab, int index, int bit) {
HashMap.TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
HashMap.TreeNode<K,V> loHead = null, loTail = null;
HashMap.TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
//在这里,根据与bit(即oldCap)进行&操作的结果是否为1,将树修剪为两部分
for (HashMap.TreeNode<K,V> e = b, next; e != null; e = next) {
next = (HashMap.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) {
//低位树的元素个数小于阈值(6个),将红黑树转换成链表
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
//低位的红黑树不变
tab[index] = loHead;
if (hiHead != null) //高位不为空,说明低位的树结构已经破坏了,需要对loHead重新树化
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);
}
}
}
在桶中的链表长度超过阈值(默认是8)时,就会将其转换成红黑树。转换过程分为两步:
1.将链表节点转换成树节点表示的链表
final void treeifyBin(HashMap.Node<K,V>[] tab, int hash) {
int n, index; HashMap.Node<K,V> e;
//在转换为红黑树时,需要判断一下当前数组的长度是否小于最小的树化容量(64),小于的话先去扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
HashMap.TreeNode<K,V> hd = null, tl = null;
do {//遍历链表,将链表的节点全部转换成树节点,形成一个新的前驱链表(prev指针)
HashMap.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);
}
}
2.树节点表示的链表转为红黑树
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;
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);
}
final void treeify(Node<K,V>[] tab) {//将链表转换成红黑树
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {//遍历链表中的每一个TreeNode,当前结点为x
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) { //对于第一个树结点,当前红黑树的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; ; ) {//从根结点开始遍历,寻找当前结点x的插入位置
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h) //如果当前结点的hash值小于根结点的hash值,方向dir = -1;
dir = -1;
else if (ph < h) //如果当前结点的hash值大于根结点的hash值,方向dir = 1;
dir = 1;
else if ((kc == null && //如果x结点的key没有实现comparable接口,或者其key和根结点的key相等(k.compareTo(x) == 0)仲裁插入规则
(kc = comparableClassFor(k)) == null) || //只有k的类型K直接实现了Comparable接口,才返回K的class,否则返回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) { //如果p的左右结点都不为null,继续for循环,否则执行插入
x.parent = xp;
if (dir <= 0) //dir <= 0,插入到左儿子
xp.left = x;
else //否则插入到右结点
xp.right = x;
root = balanceInsertion(root, x); //插入后进行树的调整,使之符合红黑树的性质
break;
}
}
}
}
moveRootToFront(tab, root); //Ensures that the given root is the first node of its bin.
}
}
其中,仲裁插入规则的过程如下: 先比较两个对象的类名,类名是字符串对象,就按字符串的比较规则。如果两个对象是同一个类型,那么调用本地方法为两个对象生成hashCode值,再进行比较,hashCode相等的话返回-1
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
移除元素时,先通过key找到对应的结点,然后再分为链表、红黑树两种情况去处理。需要注意的是,红黑树转换成链表的操作,在resize中和remove中是不同的,resize中是当红黑树中的节点少于6个时就将红黑树转为链表,而remove函数中,是当红黑树的根节点的左孩子或右孩子为空时,才将红黑树转换成链表的,此处的代码如下:
整个移除元素的过程如下:
final HashMap.Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
HashMap.Node<K,V> node = null, e; K k; V v;
//在HashMap中查找要移除的节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof HashMap.TreeNode)
node = ((HashMap.TreeNode<K,V>)p).getTreeNode(hash, key);
else {
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 HashMap.TreeNode)
((HashMap.TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//删除的节点是链表中的第一个节点
else if (node == p)
tab[index] = node.next;
//删除的节点是链表中的中间节点
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
size++
的操作,可能会导致HashMap的size出错。HashTable通过加synchronized
关键字,实现线程安全。
LinkedHashMap继承自HashMap,其在HashMap的基础上,增加了一条双向链表,使得可以保持键值对的插入顺序。如下所示:
需要注意的是,HashMap中的Entry
,继承自原来HashMap的Node
,增加了befor
和after
两个字段,用于维护双向链表。
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
除此之外,为了记录插入顺序,在LinkedHashMap中,增加了头结点和尾节点两个变量,每当有新键值对节点插入,新节点最终会接在 tail 引用指向的节点后面。而 tail 引用则会移动到新的节点上,这样一个双向链表就建立起来了。
插入数据时,使用的是HashMap的put
方法,但是重写了newNode
方法,在这里建立链表的关系。
删除元素时,使用的是HashMap的remove
方法,但是重写了afterNodeRemoval
方法,在这里维护链表的关系。
// LinkedHashMap 中覆写
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 将 p 节点的前驱后后继引用置空
p.before = p.after = null;
// b 为 null,表明 p 是头节点
if (b == null)
head = a;
else
b.after = a;
// a 为 null,表明 p 是尾节点
if (a == null)
tail = b;
else
a.before = b;
}
在访问元素的时候,有一个accessOrder
属性,当其为true
时,每次访问一个元素,都会将这个访问的元素放到尾部,保证链表尾部是最常访问的数据,可用在缓存等场景中。LinkedHashMap使用父类的HashMap来实现元素的访问,当时重写了afterNodeAccess
方法,这样如果accessOrder
为true
时,就会调用afterNodeAccess
方法,来按照访问来维护链表的顺序
// LinkedHashMap 中覆写
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
// 如果 b 为 null,表明 p 为头节点
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
// 将 p 接在链表的最后
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
基于LinkedHashMap,可以实现LRU缓存。在HashMap的putVal
函数最后,会调用afterInsertion
函数。而LinkedHashMap重写了这个函数
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
// 根据条件判断是否移除最近最少被访问的节点
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
// 移除最近最少被访问条件之一,通过覆盖此方法可实现不同策略的缓存
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
如果想用LinkedHashMap来实现LRU缓存,就直接继承LinkedHashMap,然后重写removeEldestEntry
函数,设置在某些条件下让其返回true
(比如当前集合的元素数量大于某一阈值,就返回true,否则返回false)。这样,当条件满足时,就会删除头节点。