基于jdk11
首先,我们了解一下HashMap的底层结构历史,在JDK1.8之前采用的是数组+链表的数据结构来存储数据,是不是觉得很熟悉,没错这玩意在1.8之前的结构就和HashTable一样都是采用数组+链表,同样也是通过链地址法(这里简称拉链法)来解决冲突,但是HashMap和HashTable的区别是一个是线程安全的,一个是非线程安全的。然后知道jdk1.8出来以后,HashMap做性能优化修改,底层数据结构变成了数组+链表+红黑树,性能上也有了很大改变(但还是并发问题,可能这也是为了追求性能而不改的,因为在JUC包下已经有了可以支持并发的HashMap-(ConcurrentHashMap)这个Map是支持并发操作的)。
看一下类图
两种版本的数据结构:
jdk1.8之前:
jdk1.8之后:
了解完上图后是否有疑惑但无所谓,只有看一下HashMap内部的真正数据结构就很容易理解为什么红黑树还有前后继节点
下面是HashMap存储的数据结构:
//这是数组结构,用来存储每个Node及其子类的头接节点的。
transient Node<K,V>[] table;
//单链表结构
static class Node<K,V> implements Map.Entry<K,V> {
//key的哈希码,并非节点Node的哈希码
final int hash;
final K key;
V value;
//后继节点
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;
}
}
//这是红黑树的树结构
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父母节点
TreeNode<K,V> left;//左孩子
TreeNode<K,V> right;//右孩子
TreeNode<K,V> prev; // 该节点的前继节点,然后你发现Node里面有个后继节点,这样一来这个TreeNode既是红黑树结构也是双链表结构。
//节点的颜色
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
//这个静态类是 LinkedHashMap.Entry类,可以发现它是继承于HashMap的Node类,也就是所TreeNode是Node的子类。
static class Entry<K,V> extends HashMap.Node<K,V> {
}
由代码就可以得出TreeNode是Node的子类,这也是为什么图中树节点还有指向其他节点的原因,既然HashMap有把单链表转为红黑树,那就有红黑树转为单链表,而采用这种结构为后面节点红黑树节点少于6的时候需要进行转为单链表的过程加快了转换的速度。
HashMap的数据结构就分析到这里,下面来开始分析HashMap的源码:
-
全局变量及其作用分析
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //默认初始化容量为16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //最大容量是2^30次方 static final int MAXIMUM_CAPACITY = 1 << 30; //默认负载因子是0.75 //大部分容器的负载因子都是0.75应该是经过很久的择优选择 static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认阈值为8 ,链表的节点数超过阈值就需要转换为红黑树 static final int TREEIFY_THRESHOLD = 8; //默认转换为单链表的阈值为6,即如果红黑树的节点少于6的时候会自动转为单链表 static final int UNTREEIFY_THRESHOLD = 6; //树形化的的最小容量为64,如果阈值超过8但哈希表容量值小于64一样不会转换为红黑树 static final int MIN_TREEIFY_CAPACITY = 64; //存储Node类型的节点及其子类的数组(哈希表) transient Node<K,V>[] table; //数据大小 transient int size; //操作修改的次数 transient int modCount; //k-v对数量的最大值 方后续扩容又要重新计算哈希值,所以2的幂次方会起到一定优化性能的作用,因为以2的幂次方扩容后,旧数据的位置要么在原来的位置,要么在原理的位置基础上移动+2的幂次方 这个变量的计算公式: 负载因子*容量 int threshold; //负载因子 final float loadFactor; }
由上面分析可以知道,HashMap一开始是数组+链表的方式存储,当阈值超过8和哈希表的容量达到最小树形化容量64的时候才会把该桶(链表)转换为红黑树结构,当红黑树节点低于6的时候由将红黑树切割成链表。
-
哈希码的计算与位置的关系
先观察一下源码
static final int hash(Object key) { int h; //key的哈希码与它的高16进行异或运算(低16位与高16位运算) return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } //n是哈希表的容量 下面这点代码是添加元素方法里面抽取出来的,可以看到是根据 n-1和hash进行与运算得出具体位置,只有容量是2的幂次方,计算的总是旧数组的位置或者旧数组的位置+旧容量 tab[index = (n - 1) & hash]
HashMap中haxsh(Object key)方法是根据(key的哈希码)与(key的哈希码的高16位)进行(异或运算)得到新的哈希码,但是想要确定该节点应该存储在哈希表中的那个位置还需要(新的哈希码)与(哈希表的容量-1)进行(与运算)
简单的来说元素应该存储在那个桶(那条链表或者红黑树)与key的哈希码和哈希表的容量有关
下图是大体过程
3. HashMap的扩容机制
/**
* 这个方法是其扩容或者初始化的作用,而且容量都是2的幂次方,但会尽量是接近目标容量的幂次方(只会比目标容量大的2的幂次方)
*
* @return the table
*/
final Node<K,V>[] resize() {
//首先拷贝一份哈希表到oldtab中
Node<K,V>[] oldTab = table;
//旧哈希表的容量,
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;//旧的扩容阈值
//新的容量,新的扩容阈值
int newCap, newThr = 0;
//情况1:不是初始化
if (oldCap > 0) {
//判断旧容量是否已经超过最大容量,如果超过就听天由命了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//否则,新容量=旧容量的2倍,并于最大容量比较如果小于最大容量并且旧容量大于默认初始化容量则,新容量等于旧容量的2倍。
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//情况2:哈希表没初始化,但已经把初始化容量给扩容的阈值,则新容量等于旧的扩容阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//情况3:无参构造,使用默认容量作为新容量,扩容阈值等于默认负载因子*默认初始化容量
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果新阔以阈值为0 则计算新的扩容阈值,新容量*负载因子
if (newThr == 0) {
//计算阈值
float ft = (float)newCap * loadFactor;
//做一个择优选择如果这个阈值小于最大容量则新阈值是ft,否则是整数的最大值
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。
table = newTab;
//如果旧哈希表有数据,则这里开始把旧哈希表的数据添加到新的哈希白哦
if (oldTab != null) {
//变量旧哈希表,
for (int j = 0; j < oldCap; ++j) {
//临时节点用来存储头节点(或者说每个桶中的第一个节点)
Node<K,V> e;
//把e指向桶中的第一个节点(赋值给e)
if ((e = oldTab[j]) != null) {
//然后把这个位置置空
oldTab[j] = null;
//添加到新的哈希表中
//情况1:只有一个节点的情况
if (e.next == null)
//直接计算新位置并存放头节点
newTab[e.hash & (newCap - 1)] = e;
//情况2 头节点是树形结构则使用 树的拆分方法重写映射到新的哈希表中。
else if (e instanceof TreeNode)
//拆分方法下面分析
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { //情况3:链表的情况,而且不只有一个节点
//每条链表的数据节点在新哈希表中只有两种情况:1.还在原理位置,2.在新哈希表的位置:旧位置+旧容量
//这个节点是记录节点在新哈希表还在原来位置的的第一个节点,
Node<K,V> loHead = null,
//旧数组中同一个桶在新哈希表中还是在原来桶中的节点
loTail = null;
//记录不在原理位置的节点,一个指向第一个头节点,一个用来连接
Node<K,V> hiHead = null, hiTail = null;
//下一个节点
Node<K,V> next;
do {
//记录下一个节点
next = e.next;
//如果e.hash & oldCap 进行与运算=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;
}
-
红黑色拆分
/** * 该方法是 * * @param map 当前的hashmap * @param 新的哈希表 * @param index 原位置 * @param bit 原哈希表的容量 */ final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { //记录当前桶的第一个节点(或者根节点) TreeNode<K,V> b = this; //存储重新哈希后还在原位置的节点的第一个节点(根节点) TreeNode<K,V> loHead = null, //用来存储重写哈希后还在原位置的接点。 loTail = null; //存储重新哈希后位置在(index+bit)的节点的第一个节点(根节点) TreeNode<K,V> hiHead = null, //存储重新哈希后位置在(index+bit)的节点 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;//然后把当前节点的后继节点置空 //如果节点的哈希与旧哈希表容量与运算是0则在原位置否则在index+bit位置 if ((e.hash & bit) == 0) { //如果还没有第一个节点则把loHead设置为第一个节点 if ((e.prev = loTail) == null) loHead = e; else//否则把当前节点设置为前一个节点的后继结点 loTail.next = e; //把loTail指向当前节点 loTail = e; //同时记录在原位置节点的个数为后面进行判断是否需要进行转换红黑色做准备。 ++lc; } else {//否则重写哈希位置是index+原哈希表的容量的,下面操作同上 if ((e.prev = hiTail) == null) hiHead = e; else hiTail.next = e; hiTail = e; ++hc;//记录新位置节点的个数 } } //如果原位置节点不为空 if (loHead != null) { //判断个数是否超过阈值8 if (lc <= UNTREEIFY_THRESHOLD) //不超过则调用树形结构转换为链表结构的方法,并把头节点设置为当前位置的节点 tab[index] = loHead.untreeify(map); else { //否则还是保持树形结构则直接把当前位置设置为loHead,热安徽进行红黑色转换 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 void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; //判断是否需要哈希表的长度是否小于最小树化的阈值,如果小于则并转换为树,而是进行扩容,扩容方法前面已经讲了 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); //否则已经大于最小树化的阈值则进行转换为树,并且根据哈希值计算并获取头节点赋值给e else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; //选好单链表,构造一条树形链,而且是双链表,有前后继节点 do { //根据头节点创建一颗树结构类型的树节点p,后继节点为null TreeNode<K,V> p = replacementTreeNode(e, null); //如果t1为null,则把hd树节点指向p 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); } } //这是HashMap的内部树节点类型的类的一个转为红黑树的方法 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 {//否则添加孩子节点 //记录当前节点的key,和哈希值 K k = x.key; int h = x.hash; Class<?> kc = null; //从根节点开始遍历树,寻找可以添加x节点的位置 for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key; //因为红黑树也是一颗二叉排序树,根据哈希值插入,有几种情况 //情况1:插入节点哈希值小于当前节点哈希值。标记dir为-1 if ((ph = p.hash) > h) dir = -1; //情况2:是插入节点哈希值大于当前节点哈希值,标记为1 else if (ph < h) dir = 1; //否则 哈希值相等 ,则调用compareable或者compare看key是否实现了该接口比较,如果没实现,则调用tieBreakOrder,该方法是进一步比较二者的,先获取两个对象的类名看是否是同一个类,如果是则则在调用对象生成hashcode比较。 else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) dir = tieBreakOrder(k, pk); //创建一个新节点指向当前节点 TreeNode<K,V> xp = p; //看dir标记,如果小于1则添加到当前节点的左子树,否则添加到当前节点的右子树 if ((p = (dir <= 0) ? p.left : p.right) == null) { //把插入节点的父母指向当前节点xp x.parent = xp; //再次判断标记如果小于=0 if (dir <= 0)//则把x添加到左子树 xp.left = x; else//否则右子树 xp.right = x; //以上操作都是二叉排序树的操作 //添加完后调正红黑树的平衡 root = balanceInsertion(root, x); break; } } } } moveRootToFront(tab, root); } 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; }
-
红黑色转换为链表结构
红黑树转换为链表结构则非常简单了,因为红黑树本身的数据结构定义也是一种双链表,只需要遍历转换为单链表结构并连接起来则完成
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) { //通过map的replacementNode把树形节点转换为链表节点 Node<K,V> p = map.replacementNode(q, null); //设置头节点 if (tl == null) hd = p; //否则采用尾插法添加新节点 else tl.next = p; tl = p; } //返回链表头 return hd; }
-
接着分析添加元素的put方法
大体添加元素的过程:
1.先拷贝一份哈希表到tab
然后分几种情况:
- 没初始化则调用resize(扩容也是这个方法)方法进行初始化,然后根据计算的哈希表的位置直接以创建一个需要添加的新节点赋值到该位置,添加结束
- 已经初始化,这里有分几种
- 节点已经存在,而且是头节点(第一个几点),则直接用临时节点指向它,再进行后续相关条件判断是否需要修改它的值
- 非头节点,而且该节点已经转换为树形结构(红黑树),则调用红黑树的插入方法,并让临时节点指向新添加的节点
- 非头节点,而且是链表结构,则遍历链表,如果已经存在则找出该存在的节点并用临时节点指向它,否则一种找到末尾,创建新节点用尾插入法插入添加,添加完后再判断链表的个数是否超过阈值,如果超过阈值而且哈希表的结构也超过了64(这个判断是转换树形结构的方法判断的这里只是提一下),则进行链表树形化(转换为红黑树),转换后让临时节点指向该节点
- 上面已经进行了相关操作后续指向判断是否要修改已经存在节点的值
//外部调用接口出入k-v对 public V put(K key, V value) { //调用真正的添加方法 return putVal(hash(key), key, value, false, true); } /** * Implements Map.put and related methods. * * @param hash key的哈希码 * @param key * @param value * @param onlyIfAbsen 表示是否仅在 oldValue 为 null 的情况下更新键值对的值 * @param evict 如果是true则创建新节点添加 * @return */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) { Node<K,V>[] tab;//拷贝一份哈希表到tab Node<K,V> p;//临时存储节点 int n,//哈希表的长度 i;//是记录通过哈希码计算出来的节点位置 //把tab指向哈希表并判断哈希表是否为空 if ((tab = table) == null || (n = tab.length) == 0) // 通过resize()扩容方法进行创建一个哈希表, 该方法前面面分析 n = (tab = resize()).length; //根据哈希值和哈希表的容量-1 进行与运算计算出具体散列的位置在哈希表那个位置 //并把p指向该头节点和判断是否为空,如果是空则直接创建新节点作为头节点存放在相应的位置 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else {//根节点(头节点)不为空则进行添加 Node<K,V> e; K k; //情况1:判断第一个节点是否与添加节点哈希值和key都相同,如果相同则直接e指向第一个节点 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //情况2:判断节点是否是树节点(红黑树),则调用树的添加节点 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {//情况3:链表,直接遍历链表,并且就是链表的节点个数 for (int binCount = 0; ; ++binCount) { //如果是尾节点则直接创建,并把p的后继节点指向这个新节点 if ((e = p.next) == null) { //(尾插法)添加新节点 p.next = newNode(hash, key, value, null); //看看当前节点个数是否达到了阈值 如果达到再调用转换红黑树的方法再进行相关判断,该方法后面分析 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; } } //判断e如果存在则证明原理就已经有key则返回旧值 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);// 用于LinkedHashMap return null; }
-
HashMap中获取数据的方法
public V get(Object key) { Node<K,V> e; 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,判断表是否为null,是否有数据,根据前面计算的哈希值和容量获取该key所以的桶(或链表),并判断是否为空 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //情况1:第一个节点就是则直接返回 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; }
-
HashMap的移除方法分析
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; //赋值哈希表给tab,判断表是否为null,是否有数据,根据前面计算的哈希值和容量获取该key所以的桶(或链表),并判断是否为空 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 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) 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 = 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( s, tab, movable); else if (node == p)/如果是头节点则直接让当前位置指向后一个即可 tab[index] = node.next; else//否则直接链表删除 p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; } //树节点的移除节点方法。 final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) { int n; //判断哈希表是否为null或者无数据 if (tab == null || (n = tab.length) == 0) return; //否则计算在下标位置 int index = (n - 1) & hash; first指向index位置这个桶的头节点,并且把头节点设置为根节点 TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl; //succ设置为后继节点,pred设置为前继节点 TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev; //如果前继节点是空则需要移除的节点就是头节点,则让头节点指向后继节点并把桶指向这个头节点 if (pred == null) tab[index] = first = succ; else//否则让移除节点的前继节点的后继节点指向移除节点的后继节点 pred.next = succ; //如果后继节点不为空 if (succ != null)//则让后继节点的前继节点指向移除节点的前继节点prev succ.prev = pred; if (first == null)//如果first为空,则表示删除后返回 return; if (root.parent != null)//否则父母节点不为空的,则将root指向根节点 root = root.root(); //判断是否该树是否需要转换为链表 if (root == null || (movable&& (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); }
总结:
- HashMap底层采用的数据结构是数组+链表+红黑树(红黑树有带着双链表的结构),当数组(哈希表)中的桶里的节点个数少于8个的时候会采用单链表结构存储数据,当一条链表的节点树大于8的时候将进行一次判断,看当前哈希表的个数是否超过了树形化的最小节点数(默认为64),如果链表的个数到阈值达8同时哈希表的个数也超过了64则把该链表转换为树结构的树链,再进行转换为红黑树;
- 如果链表的个数到达了阈值8但哈希表的存储的节点个数少于64则采用扩容的方式来存储,而hashmap的扩容方式的判断除了前面这种会触发扩容,还有一种就是当前哈希表的节点个数超过了扩容阈值而这个扩容阈值计算公式是:负载因子*哈希表的容量,默认负载因子是0.75,默认初始化容量是16,则添加元素的时候到达了12就需要进行扩容。
- 此外当红黑树的节点少于阈值6的时候就会把红黑树转换为单链表。
- 开头说了HashMap与HashTable结构也有类似的地方,二者都有采用数组+链表的结构,在线程安全方面,HashMap是非线程安全的,效率较高,而HasTable是线程安全的,效率较低,如果先使用效率高,而且是线程安全的可以使用JUC包下的CurrentHashMap这个并发容器类。