HashMap
上一章学习了AbstractMap,这一章我们重点学习下AbstractMap的子类HashMap,也是我们最常用的一个map。
1.HashMap声明
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {}
//HashMap的哈希桶数组,非常重要的存储结构,用于存放表示键值对数据的Node元素。
transient Node<K,V>[] table;
//HashMap将数据转换成set的另一种存储形式,这个变量主要用于迭代功能。
transient Set<Map.Entry<K,V>> entrySet;
//HashMap中实际存在的Node数量,注意这个数量不等于table的长度,甚至可能大于它,因为在table的每个节点上是一个链表(或RBT)结构,可能不止有一个Node元素存在。
transient int size;
//HashMap的数据被修改的次数,这个变量用于迭代过程中的Fail-Fast机制,其存在的意义在于保证发生了线程安全问题时,能及时的发现(操作前备份的count和当前modCount不相等)并抛出异常终止操作。
transient int modCount;
//HashMap的扩容阈值,在HashMap中存储的Node键值对超过这个数量时,自动扩容容量为原来的二倍。
int threshold;
//HashMap的负载因子,可计算出当前table长度下的扩容阈值:threshold = loadFactor * table.length。
final float loadFactor;
可以看到 HashMap继承了抽象类AbstractMap,实现了map接口,Cloneable,Serializable ,这些接口的作用很明显就不在说明了。
2.HashMap构造方法
public HashMap(int initialCapacity, float loadFactor) {
//如果初始化大小小于0,直接抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果初始化限制只能是定义好的最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//loadFactor是负载因子,负载因子大小小于等于0或者不是一个数字则会抛出异常,构造方法不能初始化对象。负载因子的作用后面会学习到。
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//重点是tableSizeFor方法
this.threshold = tableSizeFor(initialCapacity);
}
//n右移1位和自己进行或操作,得到的二进制全为1,也就是2的n次方。
先来分析有关n位操作部分:
先来假设n的二进制为01xxx...xxx。
接着对n右移1位:001xx...xxx,再位或:011xx...xxx
对n右移2为0:0011...xxx,再位或:01111...xxx
此时前面已经有四个1了,再右移4位且位或可得8个1
同理,有8个1,右移8位肯定会让后八位也为1。
综上可得,该算法让最高位的1后面的位全变为1。
最后再让结果n+1,即得到了2的整数次幂的值了。
//这里为什么要先减1呢,比如cap=8 二进制1000 运算结算为10000,为2的4次幂16,显然不是最优的答案,答案应该是其本身,所以需要先减去1
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;
}
static final int MAXIMUM_CAPACITY = 1 << 30;
//调用上面的构造方法
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//没有初始大小
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//这个方法是初始化带数据的map。这里有个重要方法putMapEntries
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
//说明是拷贝构造函数来调用的putMapEntries,或者构造后还没放过任何元素
if (table == null) { // pre-size
//根据负载因子计算出初始化的容量的临界值,因为会计算出小数因此+1.0F向上取整
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//如果计算出来的临界值是大于当前大小,重新计算临界值
if (t > threshold)
threshold = tableSizeFor(t);
}
//这是map初始化后调用的,如果传入的map大小大于初始化的临界值,进行预扩容
else if (s > threshold)
resize();
//循环数据调用putVal放入数据,putVal后面再学习
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
final Node<K,V>[] resize() {
//引用扩容前的table
Node<K,V>[] oldTab = table;
//获取扩容前的table大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//引用扩容的临界值
int oldThr = threshold;
int newCap, newThr = 0;
//如果原table大小大于0
if (oldCap > 0) {
//如果原来的大小大于等于最大容量,返回原table,并把临界值修改为2的31次方减1,这个数字远大于最大容量,以后就不会在进行扩容了。
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果原来的大小的2倍小于最大容量,并且大于16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//临界值扩大2倍
newThr = oldThr << 1; // double threshold
}
//在oldTab为null的情况下 如果 oldThr>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);
}
//newThr还没被指定吗 如果没有 则指定 也就是说在旧表有指定临界值时的时候调用
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;
//如果原来的table不为空,需要对原来的数据再散列,也就是随机放入新的table中
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果原table中只有一个元素
if (e.next == null)
//e.hash & 最大索引 & 只有存在两个一则为一 0 和本身
//再散列
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//如果e是红黑树,调用红黑树的split,这个红黑树后面有机会再学习
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//于保存put后不移位的链表
Node<K,V> loHead = null, loTail = null;
//用于保存put后移位的链表
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//如果与的结果为0,表示不移位,将table中的头结点添加到lohead和lotail中,往后如果table中还有不移位的结点,就向tail继续添加
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;
}
//table的下一个数据为null,跳出循环
} while ((e = next) != null);
if (loTail != null) {
//把不移位的结点添加到对应的链表数组中去
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
//把移位的结点添加到对应的链表数组中去,位置为当前索引加上原table大小
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
上面学习了hashMap的构造方法,最重要的是负载因子和扩容方法。需要注意的是为什么负载因子是0.75呢,这是考虑了空间利用率和hash冲突的最合适的一个扩容倍数。在理想情况下,使用随机哈希码,节点出现的频率在hash桶中遵循泊松分布,同时给出了桶中元素个数和概率的对照表。从上面的表中可以看到当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为加载因子,每个碰撞位置的链表长度超过8个的概率达到了一百万分之一。
3.put(K key, V value)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
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=null或者table大小为0,则进行table的初始化,resize()这个方法在前面已经提到过了,初始大小为16,n=16.
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//根据hash值取余定位哈希桶位置,并赋值为p,判断p不存在首节点。
if ((p = tab[i = (n - 1) & hash]) == null)
//新建节点,并放在hash所在桶的首节点位置。
tab[i] = newNode(hash, key, value, null);
else {
//到这儿了,就说明产生碰撞了,那么就要开始处理碰撞。
Node<K,V> e; K k;
// 如果哈希桶的首节点与我们待插入的数据有相同的hash和key值,则先记录。
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) {
//如果哈希桶的下一个节点为null,新建节点
p.next = newNode(hash, key, value, null);
//如果遍历到第8个节点,采用树结构
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;
//如果需要替换原来节点的值或者原来的数据为null
if (!onlyIfAbsent || oldValue == null)
//替换原节点的值
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//记录修改次数
++modCount;
//是否超过容量,超过需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
//树结构
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果元素数组为空 或者 数组长度小于 树结构化的最小限制MIN_TREEIFY_CAPACITY 默认
值64,对于这个值可以理解为:如果元素数组长度小于这个值,没有必要去进行结构转换,当
一个数组位置上集中了多个键值对,那是因为这些key的hash值和数组长度取模之后结果相同。
(并不是因为这些key的hash值相同),因为hash值相同的概率不高,所以可以通过扩容的方
式,来使得最终这些key的hash值在和新的数组长度取模之后,拆分到多个数组位置上。
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 如果元素数组长度已经大于等于了 MIN_TREEIFY_CAPACITY,那么就有必要进行结构转换了
// 根据hash值和数组长度进行取模运算后,得到链表的首节点
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;
/ 继续遍历链表,遍历完table的所有节点时停止遍历
} while ((e = e.next) != null);
// 到目前为止 也只是把Node对象转换成了TreeNode对象,把单向链表转换成了双向链表
//把转换后的双向链表,替换原来位置上的单向链表
if ((tab[index] = hd) != null)
//把双向链表转换为树结构,treeNode的内部方法后面在学习。
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);
}
//把一个map数据添加到目标map中,putMapEntries其实就是hashMap的构造方法中的一个底层实现,原理是一样的。
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
我们先一步步来分析put操作。
- hash(key)方法,计算key的hash的值,这里为什么要右移16位呢,为什么不直接用key的hashcode呢,查过资料后发现这个叫做扰动函数。大家都知道上面代码里的key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。理论上散列值是一个int型,如果直接拿散列值作为下标访问HashMap主数组的话,考虑到2进制32位带符号的int表值范围从-2147483648到2147483648。前后加起来大概40亿的映射空间,但是一个hashMap的容量不可能这么大,内存也不允许,所以这个值是不能用的。所以我们可以看到最终使用的时候会有这样一个操作(n - 1) & hash。
- (n - 1) & hash操作,假如n为初始容量大小16,n-1=15,以二进制计算为下面的操作,
01011011 10100101 11000100 00100101
&
00000000 00000000 00000000 00001111
———————————————
00000000 00000000 00000000 00000101
这样不管hashcode为多少,其实都只保留了hashcode的低位数,也就是最终就是取余。这里也解释了为什么hashMap的容量都为2的n次幂,因为这样n-1的结果低位都是1。这样的缺点就是比如16位大小的hashMap,只取最后4位很容易产生碰撞。再看h = key.hashCode()) ^ (h >>> 16 - key.hashCode()) ^ (h >>> 16 以上面的hashcode为例,01011011 10100101 11000100 00100101 ,进行下面的操作
01011011 10100101 11000100 00100101
^
00000000 00000000 01011011 10100101
———————————————
01011011 10100101 10011111 10000000
右位移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。说白了这个操作就是为了增加低位的随机性,使hash更散列。 - 继续往下看,putVal(),详细可见注释。
4.get(Object key)
//先计算key的hash值
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//获取key对应的数据节点
inal Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//如果table不为空,并且大小大于0,key所在的hash桶不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果hash桶所在的首节点的hash值和key等于key所在的hash值和key,直接返回当前节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果首节点的下一个节点不为空
if ((e = first.next) != null) {
//如果下一节点是树,用树方法获取节点
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//如果下一个节点是链表,判断hash值和key是否相同,相同即返回节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//没找到返回null
return null;
}
hashMap的get 方法其实很简单,首先计算key的 hash值 找到map中对应的hash桶,在根据桶的结构不用,使用不同的方法找到对应的节点。相同的逻辑即keyhash值相同,并且key也相同。
5.红黑树
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);
}
/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
// 把红黑树的根节点设为 其所在的数组槽 的第一个元素,首先明确:TreeNode既是一个
黑树结构,也是一个双链表结构, 这个方法里做的事情,就是保证树的根节点一定也要成为链
表的首节点
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
// 根节点不为空 并且 HashMap的元素数组不为空
if (root != null && tab != null && (n = tab.length) > 0) {
// 根据根节点的Hash值 和 HashMap的元素数组长度 取得根节点在数组中的位置
int index = (n - 1) & root.hash;
// 首先取得该位置上的第一个节点对象
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
// 如果该节点对象 与 根节点对象 不同
if (root != first) {
// 定义根节点的后一个节点
Node<K,V> rn;
// 把元素数组index位置的元素替换为根节点对象
tab[index] = root;
// 获取根节点对象的前一个节点
TreeNode<K,V> rp = root.prev;
// 如果后节点不为空
if ((rn = root.next) != null)
// root后节点的前节点 指向到 root的前节点,相当于把root从链表中摘除
((TreeNode<K,V>)rn).prev = rp;
// 如果root的前节点不为空
if (rp != null)
// root前节点的后节点 指向到 root的后节点
rp.next = rn;
// 如果数组该位置上原来的元素不为空
if (first != null)
// 这个原有的元素的 前节点 指向到 root,相当于root目前位于链表的首位
first.prev = root;
// 原来的第一个节点现在作为root的下一个节点,变成了第二个节点
root.next = first;
// 首节点没有前节点
root.prev = null;
}
// 这一步是防御性的编程, 校验TreeNode对象是否满足红黑树和双链表的特性,如
果这个方法校验不通过:可能是因为用户编程失误,破坏了结构(例如:并发下);
也可能是TreeNode的实现有问题(这个是理论上的以防万一);
assert checkInvariants(root);
}
}
//获取红黑树的指定节点
final TreeNode<K,V> getTreeNode(int h, Object k) {
// 从根节点开始查找
return ((parent != null) ? root() : this).find(h, k, null);
}
//获取红黑树指定节点
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
// p代表当前节点
TreeNode<K,V> p = this;
do {
// 定义当前节点p的hash值ph、相对位置dir、key
int ph, dir; K pk;
// 获取当前节点的左子节点、右子节点
TreeNode<K,V> pl = p.left, pr = p.right, q
// 表明目标节点在当前节点的左子节点
if ((ph = p.hash) > h)
p = pl;
// 表明目标节点在当前节点的右子节点
else if (ph < h)
p = pr;
// 当前节点的hash值与目标节点hash值相等,且当前节点的key与目标key相等(equals)
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 当前节点的hash值与目标节点hash值相等,且当前节点的key与目标key不相等(equals)
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
// 当前节点的hash值与目标节点hash值相等,且当前节点的key与目标key不相等(equals),且左子节点与右子节点均不为null,目标key实现Comparable接口,且与当前节点比较不为0
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
// 当前节点的hash值与目标节点hash值相等,且当前节点的key与目标key不相等(equals),且左子节点与右子节点均不为null,目标key没有实现Comparable接口,则直接在右子树中查询,这个方法并没有在左子树中循环,因为这是一个递归方法,先遍历右子树并判断是否查找到,若无则将左子树根节点作为当前节点,不用遍历左子树依然可以覆盖全部情况
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
// 未找到,返回null
return null;
}
/**
* Tie-breaking utility for ordering insertions when equal
* hashCodes and non-comparable. We don't require a total
* order, just a consistent insertion rule to maintain
* equivalence across rebalancings. Tie-breaking further than
* necessary simplifies testing a bit.
*/
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;
}
/**
* Forms tree of the nodes linked from this node.
* @return root of tree
*/
final void treeify(Node<K,V>[] tab) {
// 定义树的根节点
TreeNode<K,V> root = null;
// 遍历链表,x指向当前节点、next指向下一个节点
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;
// 当前节点的红色属性设为false(把当前节点设为黑色)
x.red = false;
root = x;
}
else {
// 取得当前链表节点的key
K k = x.key;
// 取得当前链表节点的hash值
int h = x.hash;
// 定义key所属的Class
Class<?> kc = null;
// 从根节点开始遍历,死循环,只能从内部跳出
for (TreeNode<K,V> p = root;;) {
// dir 标识方向(左右)、ph标识当前树节点的hash值
int dir, ph;
// 当前树节点的key
K pk = p.key;
// 如果当前树节点hash值 大于 当前链表节点的hash值
if ((ph = p.hash) > h)
//标识为左边
dir = -1;
else if (ph < h)
//标识为右边
dir = 1;
// 如果两个节点的key的hash值相等,那么还要通过其他方式再进行比较
如果当前链表节点的key实现了comparable接口,并且当前树节点和链表节
点是相同Class的实例,那么通过comparable的方式再比较两者。如果还
是相等,最后再通过tieBreakOrder比较一次
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
// 保存当前树节点
TreeNode<K,V> xp = p;
//如果dir 小于等于0 : 当前链表节点一定放置在当前树节点的左侧,但
不一定是该树节点的左孩子,也可能是左孩子的右孩子 或者 更深层次的节
点。如果dir 大于0 : 当前链表节点一定放置在当前树节点的右侧,但不
一定是该树节点的右孩子,也可能是右孩子的左孩子 或者 更深层次的节 点。 如果当前树节点不是叶子节点,那么最终会以当前树节点的左孩子或者
右孩子 为 起始节点 再从GOTO1 处开始 重新寻找自己(当前链表节点)
的位置。 如果当前树节点就是叶子节点,那么根据dir的值,就可以把当
前链表节点挂载到当前树节点的左或者右侧了。 挂载之后,还需要重新把
树进行平衡。平衡之后,就可以针对下一个链表节点进行处理了。
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);
}
/**
* Returns a list of non-TreeNodes replacing those linked from
* this node.
*/
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;
}
/**
* 当存在hash碰撞的时候,且元素数量大于8个时候,就会以红黑树的方式将这些元素组织起来
* map 当前节点所在的HashMap对象
* tab 当前HashMap对象的元素数组
* h 指定key的hash值
* k 指定key
* v 指定key上要写入的值
* 返回:指定key所匹配到的节点对象,针对这个对象去修改V(返回空说明创建了一个新节点)
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
// 定义k的Class对象
Class<?> kc = null;
// 标识是否已经遍历过一次树,未必是从根节点遍历的,但是遍历路径上一定已经包含了后续需要比对的所有节点。
boolean searched = false;
// 父节点不为空那么查找根节点,为空那么自身就是根节点
TreeNode<K,V> root = (parent != null) ? root() : this;
// 从根节点开始遍历,死循环,只能从内部退出
for (TreeNode<K,V> p = root;;) {
// 声明方向、当前节点hash值、当前节点的键对象
int dir, ph; K pk;
// 如果当前节点hash 大于 指定key的hash值
if ((ph = p.hash) > h)
// 要添加的元素应该放置在当前节点的左侧
dir = -1;
else if (ph < h)
// 要添加的元素应该放置在当前节点的右侧
dir = 1;
// 如果当前节点的键对象 和 指定key对象相同
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
// 那么就返回当前节点对象,在外层方法会对v进行写入
return p;
// 走到这一步说明 当前节点的hash值 和 指定key的hash值 是相等的,但是equals不等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
// 如果还没有比对过当前节点的所有子节点
if (!searched) {
// 定义要返回的节点、和子节点
TreeNode<K,V> q, ch;
// 标识已经遍历过一次了
searched = true;
// 红黑树也是二叉树,所以只要沿着左右两侧遍历寻找就可以了, 这是个
短路运算,如果先从左侧就已经找到了,右侧就不需要遍历了, find 方法
内部还会有递归调用。参见:find方法解析
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
// 找到了指定key键对应的
return q;
}
// 走到这里就说明,遍历了所有子节点也没有找到和当前键equals相等的节点,再比较一下当前节点键和指定key键的大小
dir = tieBreakOrder(k, pk);
}
// 定义xp指向当前节点
TreeNode<K,V> xp = p;
// 如果dir小于等于0,那么看当前节点的左节点是否为空,如果为空,就可以把要
添加的元素作为当前节点的左节点,如果不为空,还需要下一轮继续比较,如果dir
大于等于0,那么看当前节点的右节点是否为空,如果为空,就可以把要添加的元素
作为当前节点的右节点,如果不为空,还需要下一轮继续比较, 如果以上两条当中
有一个子节点不为空,这个if中还做了一件事,那就是把p已经指向了对应的不为空
的子节点,开始下一轮的比较
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 如果恰好要添加的方向上的子节点为空,此时节点p已经指向了这个空的子节点,获取当前节点的next节点
Node<K,V> xpn = xp.next;
// 创建一个新的树节点
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
// 左孩子指向到这个新的树节点
xp.left = x;
else
// 右孩子指向到这个新的树节点
xp.right = x;
// 链表中的next节点指向到这个新的树节点
xp.next = x;
// 这个新的树节点的父节点、前节点均设置为 当前的树节点
x.parent = x.prev = xp;
// 如果原来的next节点不为空
if (xpn != null)
// 那么原来的next节点的前节点指向到新的树节点
((TreeNode<K,V>)xpn).prev = x;
// 重新平衡,以及新的根节点置顶
moveRootToFront(tab, balanceInsertion(root, x));
// 返回空,意味着产生了一个新节点
return null;
}
}
}
/**
* Removes the given node, that must be present before this call.
* This is messier than typical red-black deletion code because we
* cannot swap the contents of an interior node with a leaf
* successor that is pinned by "next" pointers that are accessible
* independently during traversal. So instead we swap the tree
* linkages. If the current tree appears to have too few nodes,
* the bin is converted back to a plain bin. (The test triggers
* somewhere between 2 and 6 nodes, depending on tree structure).
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;
int index = (n - 1) & hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
if (pred == null)
tab[index] = first = succ;
else
pred.next = succ;
if (succ != null)
succ.prev = pred;
if (first == null)
return;
if (root.parent != null)
root = root.root();
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) {
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
if (replacement == p) { // detach
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
moveRootToFront(tab, r);
}
/**
* Splits nodes in a tree bin into lower and upper tree bins,
* or untreeifies if now too small. Called only from resize;
* see above discussion about split bits and indices.
*
* @param map the map
* @param tab the table for recording bin heads
* @param index the index of the table being split
* @param bit the bit of hash to split on
*/
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;
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);
}
}
}
/* ------------------------------------------------------------ */
// Red-black tree methods, all adapted from CLR
//左旋
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
// 要左旋的节点以及要左旋的节点的右孩子不为空
if (p != null && (r = p.right) != null) {
// 要左旋的节点的右孩子的左节点 赋给 要左旋的节点的右孩子 节点为:rl
if ((rl = p.right = r.left) != null)
// 设置rl和要左旋的节点的父子关系【之前只是爹认了孩子,孩子还没有答应,这一步孩子也认了爹】
rl.parent = p;
// 将要左旋的节点的右孩子的父节点 指向 要左旋的节点的父节点,相当于右孩子提升了一层,
// 此时如果父节点为空, 说明r 已经是顶层节点了,应该作为root 并且标为黑色
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
// 如果父节点不为空 并且 要左旋的节点是个左孩子
else if (pp.left == p)
// 设置r和父节点的父子关系【之前只是孩子认了爹,爹还没有答应,这一步爹也认了孩子】
pp.left = r;
// 要左旋的节点是个右孩子
else
pp.right = r;
// 要左旋的节点 作为 他的右孩子的左节点
r.left = p;
// 要左旋的节点的右孩子 作为 他的父节点
p.parent = r;
}
// 返回根节点
return root;
}
//右旋
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
// 要右旋的节点不为空以及要右旋的节点的左孩子不为空
if (p != null && (l = p.left) != null) {
// 要右旋的节点的左孩子的右节点 赋给 要右旋节点的左孩子 节点为:lr
if ((lr = p.left = l.right) != null)
// 设置lr和要右旋的节点的父子关系【之前只是爹认了孩子,孩子还没有答应,这一步孩子也认了爹】
lr.parent = p;
// 将要右旋的节点的左孩子的父节点 指向 要右旋的节点的父节点,相当于左孩子提升了一层,
// 此时如果父节点为空, 说明l 已经是顶层节点了,应该作为root 并且标为黑色
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
// 如果父节点不为空 并且 要右旋的节点是个右孩子
else if (pp.right == p)
// 设置l和父节点的父子关系【之前只是孩子认了爹,爹还没有答应,这一步爹也认了孩子】
pp.right = l;
// 要右旋的节点是个左孩子
else
pp.left = l;
// 要右旋的节点 作为 他左孩子的右节点
l.right = p;
// 要右旋的节点的父节点 指向 他的左孩子
p.parent = l;
}
//返回根节点
return root;
}
//平衡红黑树
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
// 新插入的节点标为红色
x.red = true;
//这一步即定义了变量,又开起了循环,循环没有控制条件,只能从内部跳出
xp:当前节点的父节点、xpp:爷爷节点、xppl:左叔叔节点、xppr:右叔叔节点
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
// 如果父节点为空、说明当前节点就是根节点,那么把当前节点标为黑色,返回当前节点
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
// 如果父节点不为空,并且为黑色 或者 【(父节点为红色 但是 爷爷节点为空),直接返回根节点
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 如果父节点是爷爷节点的左孩子
if (xp == (xppl = xpp.left)) {
//如果右叔叔不为空 并且 为红色
if ((xppr = xpp.right) != null && xppr.red) {
// 右叔叔置为黑色
xppr.red = false;
// 父节点置为黑色
xp.red = false;
// 爷爷节点置为红色
xpp.red = true;
// 运行到这里之后,就又会进行下一轮的循环了,将爷爷节点当做处理的起始节点
x = xpp;
}
// 如果右叔叔为空 或者 为黑色
else {
// 如果当前节点是父节点的右孩子
if (x == xp.right) {
// 父节点左旋
root = rotateLeft(root, x = xp);
// 获取爷爷节点
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//如果父节点不为空
if (xp != null) {
// 父节点 置为黑色
xp.red = false;
// 爷爷节点不为空
if (xpp != null) {
// 爷爷节点置为 红色
xpp.red = true;
//爷爷节点右旋
root = rotateRight(root, xpp);
}
}
}
}
// 如果父节点是爷爷节点的右孩子
else {
// 如果左叔叔是红色
if (xppl != null && xppl.red) {
// 左叔叔置为 黑色
xppl.red = false;
// 父节点置为黑色
xp.red = false;
// 爷爷置为红色
xpp.red = true;
// 运行到这里之后,就又会进行下一轮的循环了,将爷爷节点当做处理的起始节点
x = xpp;
}
// 如果左叔叔为空或者是黑色
else {
// 如果当前节点是个左孩子
if (x == xp.left) {
// 针对父节点做右旋
root = rotateRight(root, x = xp);
// 获取爷爷节点
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 如果父节点不为空
if (xp != null) {
// 父节点置为黑色
xp.red = false;
//如果爷爷节点不为空
if (xpp != null) {
// 爷爷节点置为红色
xpp.red = true;
// 针对爷爷节点做左旋
root = rotateLeft(root, xpp);
}
}
}
}
}
}
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,
TreeNode<K,V> x) {
for (TreeNode<K,V> xp, xpl, xpr;;) {
if (x == null || x == root)
return root;
else if ((xp = x.parent) == null) {
x.red = false;
return x;
}
else if (x.red) {
x.red = false;
return root;
}
else if ((xpl = xp.left) == x) {
if ((xpr = xp.right) != null && xpr.red) {
xpr.red = false;
xp.red = true;
root = rotateLeft(root, xp);
xpr = (xp = x.parent) == null ? null : xp.right;
}
if (xpr == null)
x = xp;
else {
TreeNode<K,V> sl = xpr.left, sr = xpr.right;
if ((sr == null || !sr.red) &&
(sl == null || !sl.red)) {
xpr.red = true;
x = xp;
}
else {
if (sr == null || !sr.red) {
if (sl != null)
sl.red = false;
xpr.red = true;
root = rotateRight(root, xpr);
xpr = (xp = x.parent) == null ?
null : xp.right;
}
if (xpr != null) {
xpr.red = (xp == null) ? false : xp.red;
if ((sr = xpr.right) != null)
sr.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateLeft(root, xp);
}
x = root;
}
}
}
else { // symmetric
if (xpl != null && xpl.red) {
xpl.red = false;
xp.red = true;
root = rotateRight(root, xp);
xpl = (xp = x.parent) == null ? null : xp.left;
}
if (xpl == null)
x = xp;
else {
TreeNode<K,V> sl = xpl.left, sr = xpl.right;
if ((sl == null || !sl.red) &&
(sr == null || !sr.red)) {
xpl.red = true;
x = xp;
}
else {
if (sl == null || !sl.red) {
if (sr != null)
sr.red = false;
xpl.red = true;
root = rotateLeft(root, xpl);
xpl = (xp = x.parent) == null ?
null : xp.left;
}
if (xpl != null) {
xpl.red = (xp == null) ? false : xp.red;
if ((sl = xpl.left) != null)
sl.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateRight(root, xp);
}
x = root;
}
}
}
}
}
/**
* Recursive invariant check
*/
static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
tb = t.prev, tn = (TreeNode<K,V>)t.next;
if (tb != null && tb.next != t)
return false;
if (tn != null && tn.prev != t)
return false;
if (tp != null && t != tp.left && t != tp.right)
return false;
if (tl != null && (tl.parent != t || tl.hash > t.hash))
return false;
if (tr != null && (tr.parent != t || tr.hash < t.hash))
return false;
if (t.red && tl != null && tl.red && tr != null && tr.red)
return false;
if (tl != null && !checkInvariants(tl))
return false;
if (tr != null && !checkInvariants(tr))
return false;
return true;
}
}
最后再来初步了解和学习下红黑树。红黑树是jdk1.8的新特性,之前是数组+链表的结构,1.8之后就是数组+链表+红黑树的结构。
- 为什么要引入红黑树 在HashMap是数组+链表的结构时会有一些缺点,当频繁出现hash冲突,有可能导致某一个hash桶位置的链表很长,这样检索性能会大大下降,所以引入红黑树,当链表的节点等于8个时,就会转化为红黑树的结构形式;当节点等于6个时,红黑树转化为链表的结构形式。
- 为什么是6和8呢
红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
还有个原因是:
中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。 - 什么是红黑树
首先得满足基本二叉树规则
(1).左子树上所有结点的值均小于或等于它的根结点的值。
(2).右子树上所有结点的值均大于或等于它的根结点的值。
(3).左、右子树也分别为二叉排序树
然后还有一些其他的特性规则
(1)每个节点要么是黑色,要么是红色。(节点非黑即红)
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。
(4)如果一个节点是红色的,则它的子节点必须是黑色的。(也就是说父子节点不能同时为红色) (5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。(这一点是平衡的关键) - 然后我们再来看上面的源码
1.TreeNode继承自LinkedHashMap中的内部类——LinkedHashMap.Entry,而这个内部类又继承自Node,所以算是Node的孙子辈了。我们再来看看它的几个属性,parent用来指向它的父节点,left指向左孩子,right指向右孩子,prev则指向前一个节点(原链表中的前一个节点),注意,这些字段跟Entry,Node中的字段一样,是使用默认访问权限的,所以子类可以直接使用父类的属性。
2.在上面的put方法中我们看到转换为树结构的方法是treeify,我们看看源码。这里的逻辑其实是这样的,循环遍历当前树,然后找到可以该节点可以插入的位置,依次和遍历节点比较,比它大则跟其右孩子比较,小则与其左孩子比较,依次遍历,直到找到左孩子或者右孩子为null的位置进行插入。 上述源码对于重要的方法都有部分注释,详情可见注释,这块比较难,有兴趣的可以多多网商找找资源,我也是在学习中还不够深入了解。主要是还是要了解hashMap的组成结构和红黑树的特点和基本思想。
6.总结
- HashMap是基于哈希表(散列表),实现Map接口的双列集合,数据结构是“链表散列”,也就是数组+链表 ,1.8之后是数组加链表+红黑树的结构。key唯一的value可以重复,允许存储null 键null 值,元素无序。
- HashMap不是线程安全的。
- 最后提一句,其实源码细节不一定非要很了解,很细节,作为大多数开发来说,知道其原理即可,有一定的了解当我们真正实用的时候可以准确的选择,遇到问题也能够知道怎么解决,尤其是性能或者线程安全方面的问题。