继承体系
重要的成员变量
/**
* 默认数组初始值大小
* 0b 0001 左移四位 0b 1000 => 0d 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会将树转化为Node链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 树化的条件之一:数组长度大于等于64才会树化,否则优先进行扩容操作
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 哈希桶(数组),由于TreeNode继承于Node,所以树化后也可以存放TreeNode节点(父类指针可以指向子类引用)
*/
transient Node<K,V>[] table;
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* 元素个数
*/
transient int size;
/**
* 当前Hash表的修改次数,元素增加或减少都会加1,替换不影响
* HashMap、TreeMap、ArrayList、LinkedList等都有modCount属性
* Fail-Fast 机制
* HashMap 不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,
* 那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。
* 这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,
* 对HashMap 内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋
* 给迭代器的 expectedModCount。在迭代过程中,判断 modCount 跟expectedModCount
* 是否相等,如果不相等就表示已经有其他线程修改了Map
*/
transient int modCount;
/**
* 扩容阈值=数组长度*负载因子,元素个数超过这个阈值会触发扩容
*/
int threshold;
/**
* 负载因子
*/
final float loadFactor;
数据节点Node和TreeNode
构造函数
/**
* 1. 给定数组初始长度和负载因子
*/
public HashMap(int initialCapacity, float loadFactor) {
// 数组初始长度不能小于 0,否则抛异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 数组的初始长度不能大于最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 负载因子小于0,或者not a number则 抛出异常,
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 设置负载因子
this.loadFactor = loadFactor;
// 计算数组的容量,暂时存到threshold(扩容阈值)
this.threshold = tableSizeFor(initialCapacity);
}
/**
* 2. 给定数组容量,用默认的负载因子0.75
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 3. 使用默认的数组长度16和负载因子0.75
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* 4. 用一个map集合去初始化另一个map集合
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
/**
* 计算一个出一个比给定值大的2的n次方的数作为数组容量
*/
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.2.3都不会创建table数组,HashMap会在第一次put数据时才创建table数组
几个关键流程
put流程
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}
/**
* onlyIfAbsent参数
* putIfAbsent方法传true,当key已经存在且value不为null时直接返回旧值,不更新
* 其他情况传false,key已经存在时更新旧值
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 临时表量 tab=》table数组,p =》临时node节点, n =》table长度, i =》临时桶下标
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 判断是否初始化table数组 table为null,长度为0 表示未初始化
if ((tab = table) == null || (n = tab.length) == 0)
// 调用扩容方法进行初始化,后面扩容部分再分析
n = (tab = resize()).length;
// 通过hash码计算出桶下标,该桶位还没有放置元素
if ((p = tab[i = (n - 1) & hash]) == null)
// 直接构造一个node节点作为首节点
tab[i] = newNode(hash, key, value, null);
// 该桶位已经有元素了(只有一个node、node链表 或者 红黑树)
else {
Node<K,V> e; K k;
// 第一个元素就发生hash冲突
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 临时变量e暂时指向key值相同的node,后续根据putIfAbsent决定是否替换旧的value值
// 桶位放置的是红黑树 (TreeNode继承于Node)
else if (p instanceof TreeNode)
// 红黑树中没有相同的key,插入成功,返回null
// 红黑树中已经存在相同的key,临时变量e暂时指向key值相同的node,后续根据putIfAbsent决定是否替换旧的value值
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 链表情况
else {
// 遍历链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { // 遍历到链表尾部,构造Node 完成插入
p.next = newNode(hash, key, value, null);
// 插入完成后检查链表长度是否超过 8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 尝试树化,树化前要检查table数组的长度是否小于64,小于64时优先扩容
treeifyBin(tab, hash);
break;
}
// 遍历链表过程中发生hash冲突,有相同的key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 直接结束循环,e已经指向了那个key相同的Node
break;
p = e; // e =>遍历的当前节点, p => 上一个节点
}
}
// e ==null 没有遇到相同的key,已经完成了插入
// e != null 遇到相同的key值,统一处理
if (e != null) { // existing mapping for key
V oldValue = e.value; // 先保存旧值
// onlyIfAbsent为false 或者 旧值为空 做更新操作
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 节点访问后序处理
return oldValue; // Hash冲突,涉及更新操作时,返回旧值
}
}
++modCount; // 插入成功(没有发生更新操作)结构修改次数加1
if (++size > threshold) // 元素个数超过阈值,触发扩容
resize();
afterNodeInsertion(evict); // 节点插入后续处理
return null; // 没有发生hash冲突,插入成功返回null
}
补充一下后续处理
以下这三个函数定义于HashMap,作用分别是:节点访问后、节点插入后、节点移除后做一些事情。LinkedHashMap继承于HashMap,重新实现了这3个函数,也就是说LinkedHashMap会用自己的实现做后置处理,而对HashMap来说啥也没做(方法体为空)!
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
reverse流程(table初始化和扩容)
扩容入口
2. 链表长度大于8,执行树化流程时发现table长度没到64,优先进行扩容
3. 插入元素后检查元素个数超过阈值threShold
先看看扩容过程中高链和低链的概念
以table长度16扩容到32为例,如图,当hash码后四位为1111时,和数组长度N-1进行与操作(N-1) & hash
得到的下标都是(16-1) & hash = 15
,扩容后这个链表的元素重新计算下标(32-1) & hash
,此时hash码的第五位参与计算,得到下标有两种情况:
- hash码第五位为0时,下标不变,还是15,这些元素组成新的链表(低链),在新数组中桶下标不变
- hash码第五位为1时,下标需要加上旧数组的长度16,等于31,这些元素组成新的链表(高链),在新数组中的桶下标加在原来的基础上加原数组的长度
HashMap扩容时不管是链表还是红黑树都通过hash & 旧数组长度
计算得到高低链,然后分别将链表放入新的桶位中,比如:
- …01111 & 1000 == 0 node放到低链
- …11111 & 1000 != 0 node放到高链
其中红黑树完成两个链表的转移之后还要进一步判断数否需要树化(拆分后的两个链表长度还有可能大于8)。
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
// 临时变量接收旧table
Node<K,V>[] oldTab = table;
// 旧table的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 旧table触发扩容的阈值
int oldThr = threshold;
// 新数组的长度和阈值都先给0
int newCap, newThr = 0;
// oldCap等于0表示初始化时调用resize,大于0就是扩容场景
if (oldCap > 0) {
// 数组长度不能超过最大值 1 << 30
if (oldCap >= MAXIMUM_CAPACITY) {
// 阈值给大,防止再次触发扩容
threshold = Integer.MAX_VALUE;
return oldTab; // 直接返回
}
// 新数组长度乘2后不超过数组的最大值 且 旧数组长度大于等于16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold 阈值翻倍
}
/*
* oldCap 等于0 初始化table场景
* 在构造函数没有给定数组大小的情况下 threshold为int类型的默认值0
* 在构造函数给出数组长度的情况下 threshold暂时存储计算出来的数组长度
* 对应的构造函数
* HashMap(int initialCapacity)
* HashMap(int initialCapacity, float loadFactor)
*/
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr; // 初始化前threshold存的是计算的数组长度
// oldThr 等于0 对应无参构造 数组长度用默认19,阈值使用16*0.75 = 12
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
/* 1. 旧数组长度不到16时触发扩容的场景
* 2. 构造函数中给出initialCapacity时初始化table场景
* 以上两个场景需要单独计算新的扩容阈值
*/
if (newThr == 0) {
// 数组长度*负载因子
float ft = (float)newCap * loadFactor;
// 判断是否到达数组长度的上限,如果到达上限直接把扩容阈值给最大值Integer.MAX_VALUE,防止再次触发扩容
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; // 接收临时变量计算好的值
@SuppressWarnings({"rawtypes","unchecked"})
// 扩容/初始化正式开始
// 构建table数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; // 赋值给table成员变量,到此初始化完成
// 扩容
// oldTab == null为初始化场景,不执行if块里的代码
if (oldTab != null) {
// 遍历旧数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 桶位不为空
if ((e = oldTab[j]) != null) {
// 临时变量e已经接收了这个同为的值,原来桶位置null,方便GC
oldTab[j] = null;
// 情况1 只有一个元素,则放到新数组对应下标的桶位即可
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 情况2 e是红黑树的头结点
else if (e instanceof TreeNode)
// 数据迁移,后面分析
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 情况三 e是链表头结点
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;
// 低链:loHead => e1 -> e2 ->...-> en <= loTail
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 高链:hiHead => e1 -> e2 ->...-> en <= hiTail
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null); // 遍历到null为止
/*
* 两个链表放到新数组对应的桶位,loHead和loTail只是收尾节点的引用,方法执行完生命周期结束
* loHead => e1 -> e2 ->...-> en <= loTail
* newTab[j] -> e1 -> e2 ->...-> en
*
* hiHead => e1 -> e2 ->...-> en <= hiTail
* newTab[j + oldCap]-> e1 -> e2 ->...-> en
*/
if (loTail != null) {
loTail.next = null;
// 低链桶下标不变
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
// 高链桶下标加旧数组的容量
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
get、replace流程
get和replace都走的是getNode方法,代码很简单
// 核心方法:getNode
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// 核心方法:getNode
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 校验table并确定桶下标
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 总是会判断第一个是不是目标,正好是就返回首节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 判断有没有第二个,没有就返回null,有就区分链表和红黑树两种场景找目标节点
if ((e = first.next) != null) {
if (first instanceof TreeNode) // 红黑树调用getTreeNode
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do { // 链表,利用next指针依次迭代
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
remove流程
讨论一个问题,hashmap在put元素时会触发自动扩容,那么remove元素时会不会触发自动缩容呢?
答案是不会,在知乎上有一个关于对缩容存在的意义的讨论:为什么java的hashmap不支持动态缩小容量?
总结来说就是没有什么必要,真正遇到table数组很大占用着较大的内存空间而存储的元素却没几个的情况,完全可以手动处理(新创建一个HashMap,并把数据迁移过来即可)。
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
/**
* Implements Map.remove and related methods.
*
* @param hash key的hash值
* @param key 要删除的元素的key
* @param value 要删除的元素的value,matchValue参数为true时提供,元素的值匹配上才能删除
* @param matchValue 是否值匹配才删除
* @param movable 删除后是否移动节点
* @return 返回删除的节点,删除失败返回null
*/
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;
// 先查
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;
// 老套路了,先看首节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p; // 从这里直接跳到删除逻辑,在删除逻辑中通过 node == p 判断是不是删除的首节点
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((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.next == e
// 如果找到了node,则 p.next == e, 同时p.next == node,这个关系用在删除逻辑中
p = e;
} while ((e = e.next) != null);
}
}
// 查到的目标节点node
// 再删
/**
* 如果不需要匹配value,直接删除
* 如果需要匹配value,“==” 或者 equals了才删
*/
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
/* 走红黑树删除流程,movable参数表示删除后是否移动节点
* 参数this代指HashMap对象
* 为什么参数中不传递要删除的元素node QAQ
* 因为是这样node.removeTreeNode调用的,所以在removeTreeNode里面this就是要删除的节点了
*/
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p) // 要删除的节点node是首节点
tab[index] = node.next; // 将node.next作为首节点,方法执行完node指向的对象进入GC
else // 删除前 p.next == node,删除后 p.next == node.next,方法执行完node指向的对象进入GC
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
treeifyBin
table某个桶位node链表长度超过8时,会调用treeifyBin,检查table数组长度达到64的情况下,先把原来的单链表结构变成了双链表结构,然后调用TreeNode::treeify(table)进行树化。红黑树的结构同时维持着双链表的结构,
/**
* tab:table数组
* hash:hash值(增加的键值对的key的hash值)
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// table数组长度不到64时,优先进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 获取首节点,判断不为空
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 定义头结点head和尾结点tail
TreeNode<K,V> hd = null, tl = null;
// do-while循环 将把Node对象转换成了TreeNode对象,把单向链表转换成了双向链表:
// hd => p1 <=> p2 <=> ... <=> pn <= tl
do {
// 将该Node节点转换为TreeNode节点
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);// 双向链表转红黑树,后面分析
}
}
// 将该Node节点转换为TreeNode节点
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
简单理解下单链表转双链表的过程
- 首先将Node节点转换为TreeNode节点,TreeNode节点中的next和prev分别维护双向链表的两个指针
- hd和tl分别指的是双向链表头尾两个节点的引用,给tl赋值prev和next,就等于赋值最后一个节点赋值。
- 方法执行完hd和tl作为局部变量生命周期结束,双链表的头结点的地址放在了table的对应桶位
tab[index] = hd
// TODO if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
这个tab==null的判断对应的场景暂时没搞清楚 后面搞懂了再补充
红黑树的相关操作
TreeNode::putTreeVal
put流程调用putTreeVal将元素插入红黑树
/**
* map => HashMap对象
* tab => table
* h => hash值
* k => key
* v => value
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
// 标识是否被搜索过,后面用到再做说明
boolean searched = false;
// 查找根节点, 索引位置的头节点并不一定为红黑树的根节点
TreeNode<K,V> root = (parent != null) ? root() : this;
// 根节点赋值给临时变量p,二分查找
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
// 传入的hash值h小于p节点的hash值,将dir赋值为-1,代表向p的左子树查找
if ((ph = p.hash) > h)
dir = -1;
// 传入的hash值大于p节点的hash值, 将dir赋值为1,代表向p的右子树查找
else if (ph < h)
dir = 1;
// 传入的hash值h等于p节点的哈希值,进一步判断key值等于p节点的key值, 如果相等就是找到与要插入的key相同的节点。将这个节点返回,在上层方法中决定是否替换value值
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
/* 走到这里说明hash相同而key不相同, ph == h,pk !=k, k.equals(pk) == false
* 这意味着涉及到传入的k于当前节点key值pk之间的比较了
* 判断:
* 如果没有实现Comparable<C>接口或者 实现该接口 并且 k与pk Comparable比较结果相同
* 否则就是实现了comparable接口且比较不相等,直接到插入流程
*/
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
// 由于要用递归的方式在左右子树中搜索,所以如果已经搜索过,那么把searched设置为true,避免下次循环重复搜索
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
// 在左右子树递归的寻找 是否有key的hash相同 并且equals相同的节点
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
// 找到了 就直接返回
return q;
}
// 说明红黑树中没有与之equals相等的 那就必须进行插入操作 必须将两个key分出大小 dir的结果必须是-1或1
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
/*
* 如果dir <= 0
* p.left == null,把要添加的元素作为当前节点的左节点
* p.left != null,p = p.left,下一轮循环
* 如果dir>0
* p.right == null,把要添加的元素作为当前节点的右节点
* p.right !=null,p = p.right,下一轮循环
*/
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 到这里p已经为null,且xp为p的父节点,至少一个子树是null
Node<K,V> xpn = xp.next;
// 创建一个新的树节点,注意这里设置了x.next为xpn,如果xpn不为null,name插入完成后还要设置xpn.prev为x
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x; // 新节点作为左孩子
else
xp.right = x; // 新节点作为右孩子
// 维护双链表 并设置新节点的父节点
xp.next = x; // xp.next可能之前就有值,暂时保存在xpn中
x.parent = x.prev = xp;
// 如果原来的xp.next,即xpn节点不为空时,下面操作相当于把xpn接在了x后面
if (xpn != null) // 新节点覆盖了之前xp.next 的值
/*
* 原来的xp.next 是 xpn
* 现在xp.next是x,x.prev 是 xp,xpn.prev 是 x,x.next早在创建x时就设置为xpn了
((TreeNode<K,V>)xpn).prev = x;
// balanceInsertion 重新平衡
// 经过平衡,根节点可能已经过改变,为了查询方便需要把新的根节点放到table数组桶上
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
补充说明:
-
dir = tieBreakOrder(k, pk);
hashCode一般是经过重写的,而且在HashMap中经过了二次哈希得到key的hash,这个方法调用identityHashCode获取k和pk重写前对象的哈希值来做比较。
一句话解释identityHashCode是根据Object类hashCode()方法来计算hash值,无论子类是否重写了hashCode()方法。而obj.hashcode()是根据obj的hashcode()方法计算hash值 -
for (TreeNode<K,V> p = root;;) {...}
比较key是否相等,用到一个约定:对象相等,hash一定相等,hash相等,对象不一定相等,这个技术细节总结在hashCode和equals中,所以在上面源码中到了hash相等的情况需要进一步判断key值是否equals,如果key不equals,必须想办法分出大小(调用dir = tieBreakOrder(k, pk);比较重写前对象的hash值),做插入操作。但是,在这之前需要先判断之前有没有插入过重复的key,然后进入第3条的描述。 -
需要再判断是否实现Comparable接口,没有实现Comparable接口或者Comparable比较结果相同,这种情况只能从子树中找是否有key的hash相同 并且equals相同的节点(如果之前这种情况发生,那肯定也是没找到才插入到子树中的,那这一次肯定先找再插入),找到了返回。其他情况调用dir = tieBreakOrder(k, pk);比较重写前的hash,区分出大小后插入。
-
在红黑树中插入元素后还要维护双向链表,
x节点插入到xp节点的子树时,假设xp节点的后继节点xpn是null,则将x放到双链表后面即可
x节点插入到xp节点的子树时,假设xp节点的后继节点xpn不为空,则将x放到双链表xp和xpn之间
下面两个方法,插入、查询、树化操作会用到
static Class<?> comparableClassFor(Object x) 大佬的文章
static int compareComparables(Class<?> kc, Object k, Object x) 比较简单,返回compareTo的比较结果
下面方法,插入、树化操作会用到,查询不用的原因看后面getTreeNode中的分析
static int tieBreakOrder(Object a, Object b)
TreeNode::treeify
将双链表转为红黑树
小细节: 正如章节标题TreeNode::treeify的意思,treeify的作用域是TreeNode类,调用它必然是TreeNode对象.treeify
,那么这个方法内部的this就指代这个TreeNode对象。
调用链表首节点.treeify
,则this就指代首节点了,后续节点用next指针迭代就可以了。
/**
* Forms tree of the nodes linked from this node.
*/
final void treeify(Node<K,V>[] tab) {
// 定义树的根节点
TreeNode<K,V> root = null;
// 遍历链表,x当前节点,this为首节点,next下一个节点
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
// 某个桶位扩容前是红黑树,扩容后还是红黑树的情况下,x的左右子树不为null。
x.left = x.right = null;
// 第一次循环还没有设置根节点
if (root == null) {
x.parent = null; // 根节点没有父节点
x.red = false; // 根节点一定是得是黑色
root = x; // 设置根节点
}
// 第二次以后的循环已经存在根节点了
else {
// 获取当前链表节点的key和hash值
K k = x.key;
int h = x.hash;
// 定义当前链表节点key的运行时类型
Class<?> kc = null;
// 遍历插入红黑树
for (TreeNode<K,V> p = root;;) {
// dir 插入方向, ph 当前树节点的hash值
int dir, ph;
K pk = p.key; // pk 当前树节点的key
// 如果当前树节点hash值 > 当前链表节点的hash值
if ((ph = p.hash) > h)
dir = -1; // 标识向左树插入
else if (ph < h)
dir = 1; // 右树
/* ph = h,hash值相等 做以下判断(PS:和putTreeVal过程不同的是,这里不用判断key是否相等,因为已经形成链表了,key必然不会相等了)
* kc == null &&(kc = comparableClassFor(k)) == null)
* true:当前链表节点的key没有实现comparable,直接执行dir = tieBreakOrder(k, pk);
* false:实现了comparable接口,执行 || 后面的表达式
* dir = compareComparables(kc, k, pk)) == 0
* true:通过compareTo比较大小,结果为0,没办法确定dir正负值,执行dir = tieBreakOrder(k, pk);
* false:已经确定dir的正负值,即确定了插入左边还是右边
*/
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;
}
}
}
}
// 目前放在table数组桶上的是链表的第一个节点,经过多次平衡这个节点很有可能不是root节点了,为了查询方便需要把root节点放到table数组桶上
moveRootToFront(tab, root);
}
TreeNode::untreeify
/**
* 反树化过程中将TreeNode转换成Node节点
* 从结果来看TreeNode节点转为Node节点,TreeNode额外的属性没了,next属性不变
* 即双链表转为单链表next维护的顺序没变
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
// 定义指向node链表头尾的指针(引用)
Node<K,V> hd = null, tl = null;
// q:遍历的当前节点,this:链表首节点(上层方法中首节点调用的untreeify)
// q定义为Node,实际对象的类型为TreeNode
for (Node<K,V> q = this; q != null; q = q.next) {
// 将q节点从TreeNode转为Node p
Node<K,V> p = map.replacementNode(q, null);
// 单链表: =>表示引用,->表示next的指向
// tl => p1 -> p2 -> ... -> pn <= tl
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
// TreeNode转为Node
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
return new Node<>(p.hash, p.key, p.value, next);
}
TreeNode::split
/**
* 扩容过程中的红黑树元素的转移
*
* @param map 当前Map
* @param tab 扩容后的新table
* @param 旧table中红黑树的索引位置
* @param bit 扩容前的容量
*/
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
// this:第一个节点(上层方法中是第一个节点调用的split)
// 由于红黑树中插入元素和链表的树化操作都会平衡和root节点置顶,所以桶上的第一个节点肯定是root节点,但不一定是双链表的首节点
TreeNode<K,V> b = this;
// 定义高低链表首尾节点的引用
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0; // 定义高低链表的长度
// 循环的当前节点e 下一个节点next
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
// 原来保存的next属性置空(因为第一次循环不会重新赋值),prev属性每次循环都会重新赋值
e.next = null;
// 低链
if ((e.hash & bit) == 0) {
// 第一次遍历 loHead loTail 都是null
if ((e.prev = loTail) == null) // 设置最后一个节点的prev属性
loHead = e;
else
loTail.next = e; // 设置倒数第二个节点的next属性
loTail = e; // loTail从倒数第二个节点指向最后一个节点(第一次循环从null指向e1)
++lc; // 链表长度加1
/* 第一次循环执行结果:loHead => e1 <= loTail
* e1(即loTail)的next和prev暂时都指向自己
* 第n次循环执行结果:loHead => e1 <-> e2 <->...<-> en <= loTail
* en(即loTail)的next在最开始已经置为null,下次循环else块中才会设置(但是没有下次循环了)
*/
}
// 高链,同上
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
// 低链元素个数小于等于 6
if (lc <= UNTREEIFY_THRESHOLD)
// 红黑树转化为单链表, 低链在新table的下标不变
tab[index] = loHead.untreeify(map);
else {
// 先将双链表放到数组上
tab[index] = loHead;
// 高链为空说明之前的红黑树节点都在低链上,上面也分析了遍历的第一个节点是root节点也成为了双链表首节点,上面的操作只是让双链表的顺序发生了变化,无需操作
if (hiHead != null) // (else is already treeified)
// 高链不为空则需要重新进行树化操作
loHead.treeify(tab);
}
}
// 同上,高链放置的桶下标 index + bit
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
TreeNode::getTreeNode
/**
* 调用TreeNode::find方法
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
// root() 返回根节点
return ((parent != null) ? root() : this).find(h, k, null);
}
/**
* h k的hash值
* k 元素的key
* kc k的运行时类型
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
// 获取当前节点
TreeNode<K,V> p = this;
do {
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值相等时,key相等,则找到目标节点,返回
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
/*
* ph == h 且 k.equals(pk) == false时 才会进入以下几个判断
* 根据之前的putTreeValue代码分析可知,如果树中存了这个key,那么必然存在当前节点的子树中
*/
// 左树都已经是null了,在之后的代码中判断向左子树还是右子树查询或者对pl递归查就没有意义了,直接将右子节点作作为当前节点,进入下一次循环,
else if (pl == null)
p = pr;
// 同上
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
// 到这里说明,左子节点与右子节点均不为null,目标key实现了Comparable接口,且与当前节点比较不为0,确定了从左子树查找还是从右子树查找
p = (dir < 0) ? pl : pr;
/* 目标key没有实现Comparable接口,或者comparaTo比较出来是0,则直接在右子树中查询,
* 这个方法并没有在左子树中循环,因为这是一个递归方法,先遍历右子树并判断是否查找到,若无则将左子树根节点作为当前节点,不用遍历左子树依然可以覆盖全部情况
*/
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
说明:
- 所有找到目标节点的代码位置都是这一行
else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p;
- 结合插入的过程,当遇到key的hash相同但不equals时,对于
dir = tieBreakOrder(k, pk);
确定出插入到左子树还是右子树的节点来说,查询时并没有用同样的方法确定向左查还是向右查,而是采用了递归的方式,递归时,先遍历右子树并判断是否查找到,如果没找到则将左子树根节点作为当前节点,不用遍历左子树依然可以覆盖全部情况