一. 插入
1.1 主流程解析
public static void main(String[] args) {
HashMap<Object, Object> map = new HashMap<Object, Object>();
// 重写Entity的hashCode方法,返回传入的数值
map.put(new Entity(97), "2");
// 插入a,hashCode值同样为97,模拟相同hashCode的值插入,形成链表
map.put("a", "1");
// 再次插入a,模拟相同hashCode且满足==,或equals,替换原值并返回
map.put("a", "1");
// 增加多个值,模拟扩容
for(int i = 'b'; i < 'q'; i++){
map.put(String.valueOf((char)i), i);
}
}
调用put方法
public V put(K key, V value) {
// 计算hash值,调用key.hashCode() ^ (key.hashCode() >>> 16)
// hashCode和hashCode右移16位进行位运算,增加离散性
return putVal(hash(key), key, value, false, true);
}
利用算好的hash值,开始插入数据
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数组大小是16【DEFAULT_INITIAL_CAPACITY】
// 默认的扩容因子是0.75【DEFAULT_LOAD_FACTOR】,即16 * 0.75
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 根据容量(n - 1) & hash,算出一个小于n的数组下标,判断当前下标对应的tab是不是为空,
// 如果为空,那么直接新建一个Node放到该数组位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 如果当前下标已经有一个Node了
Node<K,V> e; K k;
// 判断hash值知否一样,并且key是不是同一个对象或这个equals相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果相等,就把当前下标的Node赋给临时变量e
e = p;
else if (p instanceof TreeNode)
// 如果当前下标的Node是一个TreeNode,说明是一个红黑树的Node,然后往红黑树中插入数据,插入逻辑就移到TeeNode中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 否则当前下标的Node是一个链表结构
// 从头部遍历链表
for (int binCount = 0; ; ++binCount) {
// 如果没有下一个节点了,表示前面的遍历都没有遇到key相等或equal的,说明该节点已经是尾节点了,那么就把尾节点的下一个节点设置为当前需要插入的节点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 判断节点数有8个了,那么就将链表转换成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果有相同或equals的key,那么就break掉,此时e就是该相同key的Node
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果e不为null,说明链表中有相同的key,直接替换该值并返回,不会对modCount或者size执行++操作
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 成功插入数据,除开相等或equals的key,modeCount都要++
++modCount;
//size也执行++操作,并且比较是否大于了扩容阈值,然后进行自动扩容操作
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
上面是HashMap插入的大致插入流程,下面我们来具体分析每一步
1.2 初始化table
初始化的过程其实分为两步,第一步是在实例化HashMap的时候,HashMap的构造方法分为无参构造和有参构造
1.2.1 构造器初始化
// 无参构造方法默认会初始化扩容阈值因子,loadFactor=0.75,后续初始化时默认使用16作为数组初始化容量
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 有参构造方法,可以传入一个容量大小,扩容因子
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;
// tableSizeFor方法中会进行一些列的位运算,最终返回一个小于等于initalCapacity的2的幂次方的容量值,后续resize的时候会用到
this.threshold = tableSizeFor(initialCapacity);
}
1.2.1 插入过程中的初始化
数组的初始化是在put的时候进行的,当发现当前table为空会长度为0时,会进行重新计算数组长度。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
下面来看resize方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// 获取到原数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 获取到原阈值,如果是通过有参构造器创建的hashMap,这个值会是一个2的幂次方的数值,当第一次进这个方法的时候就是将要初始化的数组的长度,而不是乘以因子后的阈值
int oldThr = threshold;
int newCap, newThr = 0;
// 如果原容量大于0,表示初始化过了
if (oldCap > 0) {
// 长度不能超过2^32次方个
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// oldCap左移以为表示 乘以 2,原来的扩容阈值也翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 如果没初始过,并且原来的阈值大于0,那应该就是通过有参构造函数实例化的HashMap,直接将扩容阈值给新数组的容量
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) {
// 遍历原来长度的数组,决定该下标的Node应该放到新的数组的哪个下标
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
// 先把原下标置为null
oldTab[j] = null;
// 如果当前下标只有一个Node,没有链表也没有红黑树,那么就用hash值和新数组长度-1做与运算,
// 得到一个小于新数组长度的下标,并放进去
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果当前下标的Node是红黑树结构,就拆分红黑树,这里我们等到面说了红黑树再来分析
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;
// hash值和原数组长度做与运算,长度是一个2的幂次方,因此只有可能有一个位上是1
// 因此这个与运算,要么是等于0,要么是等于原数组长度
// 如果等于0,就放到低位,如果不等于0,就放到新扩容的高位,位置为原位置 + 原数组长度
// 这样就完成了原链表的拆分
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;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
1.2.2 链表转红黑树
前面我们提到当同一个下标上的链表的Node个数大于等于了8个,那么就会将链表转换成红黑树,但实际上,这么回答不对,大于等于8只是会进入下面的treeifyBin方法,但这个方法还有一个判断条件必须是数组的长度要大于等于64【MIN_TREEIFY_CAPACITY】才会转换红黑树,否则hashMap会认为当前数组还太小,可以通过扩容解决。
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();
// (n - 1) & hash就是当前链表上Node数等于8的数组下标,获取到这个下标的node
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 头节点,尾节点
TreeNode<K,V> hd = null, tl = null;
do {
// 利用原Node的hash,value,key, next创建一个新的TreeNode
TreeNode<K,V> p = replacementTreeNode(e, null);
// 第一次循环把原链表的作为head节点产生的TreeNode赋值给hd,尾节点也指向它
if (tl == null)
hd = p;
// 后面的循环将当前遍历的节点的prev指向上一次循环种的尾节点,上一次循环的尾节点指向当前节点
else {
p.prev = tl;
tl.next = p;
}
// 并把尾节点指向当前节点
tl = p;
} while ((e = e.next) != null);
// 上面循环完成后形成了另一个TreeNode的双向链表
// 还是按原来的格式,将头节点放入数组中,即头插法
if ((tab[index] = hd) != null)
// 产生红黑树
hd.treeify(tab);
}
}
看生成红黑树之前,我们先看下TreeNode类
TreeNode<K,V> parent; // red-black tree links 父节点
TreeNode<K,V> left; // 左边子节点
TreeNode<K,V> right;// 右边子节点
TreeNode<K,V> prev; // 双向链表用的上一个子节点
boolean red; // 是否为红节点
红黑树的定义或者说是规则如下:
- 每个结点或是红的,或是黑的
- 根节点是黑的
- 每个叶结点是黑的,null leaves
- 如果一个结点是红的,则它的两个儿子都是黑的
- 对每个结点,从该结点到其子孙节点的所有路径上包含相同数目的黑结点
下面来看TreeNode是怎么利用头节点的treeify方法创建出这个树的
final void treeify(Node<K,V>[] tab) {
// 根节点临时变量
TreeNode<K,V> root = null;
// 遍历treenode链表,一个一个插入到红黑树种
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
// x为当前节点,next为下一个节点
x.left = x.right = null;
// 当遍历的x为头节点时root==null
// 设置它的父节点为null,不为红节点,root定位头节点
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
// 当后续进入该节点时
K k = x.key;
int h = x.hash;
Class<?> kc = null;
// 从根节点开始比较hash值的大小
for (TreeNode<K,V> p = root;;) {
// 临时变量标识方向和当前遍历到的节点的hash值
int dir, ph;
K pk = p.key;
// 如果遍历到的树节点的hash值大于当前待插入的节点,标识需要继续和左边子节点进行比较
// 如果相反,则和右边子节点继续比较
// 如果相等,看待插入节点是否实现了Comparable,如果没有实现或者实现了但是调用compareTo比较结果还是等于0,那么就调用tieBreadOrder方法继续比较
// 如果比较的其中一个有一个为null或者类名称相同,就调用System.identityHashCode方法进行比较,反正最后得出一个比较值,如果还是等于0,就让它为0,默认走比较左子节点的逻辑
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);
// xp临时变量,标识待插入节点的父节点
// 根据上面判断出来的方向,看它的左右子节点是否存在,如果存在则继续比较,如果不存在
// 将当前遍历到的树节点作为待插入节点的父节点
// 并且根据方向,将当前遍历的树节点根据方向将左或右子节点置为待插入节点
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节点
root = balanceInsertion(root, x);
break;
}
}
}
}
// 得到的root对应的node放回数组的对应下标上
moveRootToFront(tab, 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;;) {
// 如果当前节点的父节点为null,说明当前节点就是根节点,将自己置为黑色返回
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
// 如果当前节点的父节点为黑色,或者祖父节点为null,说明新插入的节点的为第二层节点,上层就是root节点,直接返回父节点root节点
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 {
// 如果当前节点是父节点的右子节点,先左旋,再右旋
// 如果是左子节点,那么直接右旋
// 得到新的root节点后,继续递归平衡
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);
}
}
}
}
}
}
左旋,其中参数说明一下:
root:当前的根节点
p:需要进行左旋的节点
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) {
// 将需要左旋的节点的右子节点指向原右节点的左节点,如果不为空,再将这个右子节点的父节点指向当前节点
if ((rl = p.right = r.left) != null)
rl.parent = p;
// 如果当前节点的父节点为null,说明p节点是root节点,那么就将它的右节点置为root节点并置为黑色
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
// 如果p不是root节点,p是它父节点的左节点,那么直接将pp的左节点指向p的右节点r
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
// 把最终得到的r的左节点指向当前节点完成左旋
r.left = p;
p.parent = r;
}
return root;
}
右旋的操作逻辑自己体会一下,这里就不概述了。
最后得到一颗完整的红黑树结构,返回了他的root节点,
然后把root节点对应的node设置到对应的数组下标上。
1.2.3 插入到已生成的红黑树
前面主流程中会判断当前下标的节点是否是instanceof TreeNode,如果是说明这个下标上已经是红黑树,那么本次插入会直接将节点往红黑树中插。
调用TreeNode【root节点】的putTreeVal方法插入。
/**
* Tree version of putVal.
*/
// map:hashMap实例
// tab:数组
// h: hash值
// k:键
// v:值
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;
// 拿到root节点,一般就是自己
TreeNode<K,V> root = (parent != null) ? root() : this;
// 这里的逻辑基本和treeify方法一样,判断出本次插入的这个节点的hash值,是该往左边查还是往右边查,如果遍历到的节点刚好有这个方向的子节点,就递归比较,循环中主要做插入位置的判断,其他操作还是一样交给balanceInsertion,然后再设置root节点对应的Node到数组上
// **另外,这里还有一个很大的不同是**:判断遍历到的节点hash值如果和当前节点的hash值相等,并且两个key的对象是同一个对象或者equals,那么直接返回该节点值,不会做任何插入操作,而是直接在外层方法中进行值替换
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
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;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
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;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
// 平衡调整及设置数组下标对应的root节点
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
好了,插入的部分基本上讲到这里,下面我们来看查询的部分,如果能理解插入的逻辑,查询就比插入简单多了。
二. 查询
2.1 主流程
查询的基本思路:
- 通过hash值,根据容量做与运算,获取到这个hash值对应的数组下标,
- 比较判断节点类型
- 遍历比较key是否是同一对象或是否equals,拿出对应的Node节点或者TreeNode节点
- 获取value
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 校验及获取到对应下标上的
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;
if ((e = first.next) != null) {
// 如果是红黑树,调用红黑树的获取树节点方法
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;
}
其实基本上查询的代码就差不多结束了,只有treeNode需要再次进getTreeNode获取节点,稍微想一下就应该知道里面的逻辑是什么,无非就是递归判断树节点和当前要查询的节点的hash值大小,选择方向,继续递归,一旦查询到有满足条件的key,就返回该节点。
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;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 如果既不大于也不小于,还不等于,那么判断是否有左子节点是否为kong,取右节点继续向下遍历
else if (pl == null)
p = pr;
// 取左节点继续往下遍历
else if (pr == null)
p = pl;
// 比较compareTo,能比较出大小,继续向下遍历
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
// 所有上述条件都得不到一个,默认使用右节点查找,再找不到就返回null
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
今天就先写到这里,后面还涉及到HashMap的删除操作,也是一个比较复杂的过程,下次慢慢写。