java源码--HashMap
一、HashMap简介
1.1、HashMap概述
HashMap是基于哈希表的Map接口实现的,它存储的是内容是键值对<key,value>映射。此类不保证映射的顺序,假定哈希函数将元素适当的分布在各桶之间,可为基本操作(get和put)提供稳定的性能。
1.2、HashMap在JDK1.8以前数据结构和存储原理
1)链表散列:通过数组和链表结合在一起使用,就叫做链表散列。如下
2)HashMap的数据结构和存储原理
HashMap的数据结构是链表散列。那HashMap底层是怎么样使用这个数据结构进行数据存取的呢?分成两个部分:
第一步:HashMap内部有一个entry的内部类,其中有四个属性,我们要存储一个值,则需要一个key和一个value,存到map中就会先将key和value保存在这个Entry类创建的对象中。
static class Entry<K,V> implements Map.Entry<K,V> { final K key; //就是我们说的map的key V value; //value值,这两个都不陌生 Entry<K,V> next;//指向下一个entry对象 int hash;//通过key算过来的你hashcode值。
Entry的物理模型图:
第二步:构造好了entry对象,然后将该对象放入数组中,如何存放就是这hashMap的精华所在了。
大概的一个存放过程是:通过entry对象中的hash值来确定将该对象存放在数组中的哪个位置上,如果在这个位置上还有其他元素,则通过链表来存储这个元素。
3)Hash存放元素的过程
通过key、value封装成一个entry对象,然后通过key的值来计算该entry的hash值,通过entry的hash值和数组的长度length来计算出entry放在数组中的哪个位置上面,
每次存放都是将entry放在第一个位置。在这个过程中,就是通过hash值来确定将该对象存放在数组中的哪个位置上。
1.3、JDK1.8后HashMap的数据结构
上图很形象的展示了HashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表,也可能是红黑树,红黑树的引入是为了提高效率。
1.4、HashMap的重要属性介绍
HashMap的实例有两个参数影响其性能。
初始容量:哈希表中桶的数量
加载因子:哈希表在其容量自动增加之前可以达到多满的一种尺度
当哈希表中条目数超出了当前容量*加载因子(其实就是HashMap的实际容量)时,则对该哈希表进行rehash操作,将哈希表扩充至两倍的桶数。
Java中默认初始容量为16,加载因子为0.75。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 static final float DEFAULT_LOAD_FACTOR = 0.75f;
1)loadFactor加载因子
定义:loadFactor译为装载因子。装载因子用来衡量HashMap满的程度。loadFactor的默认值为0.75f。计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。
loadFactor加载因子是控制数组存放数据的疏密程度。
loadFactor越趋近于1:那么数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加;
loadFactor越小,也就是趋近于0:那么数组中存放的数据也就越稀,也就是可能数组中每个位置上就放一个元素。
那有人说,就把loadFactor变为1最好吗,存的数据很多,但是这样会有一个问题
就是我们在通过key拿到我们的value时,是先通过key的hashcode值,找到对应数组中的位置,如果该位置中有很多元素,则需要通过equals来依次比较链表中的元素,拿到我们的value值,这样花费的性能就很高。
如果能让数组上的每个位置尽量只有一个元素最好,我们就能直接得到value值了,所以有人又会说,那把loadFactor变得很小不就好了,但是如果变得太小,在数组中的位置就会太稀,也就是分散的太开,浪费很多空间,这样也不好。
所以在hashMap中loadFactor的初始值就是0.75,一般情况下不需要更改它。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
2)桶
根据前面画的HashMap存储的数据结构图,你这样想,数组中每一个位置上都放有一个桶,每个桶里就是装一个链表,链表中可以有很多个元素(entry),这就是桶的意思。也就相当于把元素都放在桶中。
3)capacity
capacity译为容量代表的数组的容量,也就是数组的长度,同时也是HashMap中桶的个数。默认值是16。
一般第一次扩容时会扩容到64,之后好像是2倍。总之,容量都是2的幂。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
4)size的含义
size就是在该HashMap的实例中实际存储的元素的个数
5)threshold(临界值)的作用
threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是衡量数组是否需要扩增的一个标准。
临界值= 16*0.75=12 也就是说,当size>=12的时候,考虑进行扩容
注意这里说的是考虑,因为实际上要扩增数组,除了这个size>=threshold条件外,还需要另外一个条件。
什么时候会扩增数组的大小?在put一个元素时
1、 size>=threshold 2、还要在对应数组位置上有元素,这才能扩增数组。
int threshold;
通过一张HashMap的数据结构图来分析:就是想要扩容 真实的size大小要大于临界值threshold
二、HashMap源码
2.1、HashMap的层次关系与继承结构
1)HashMap继承结构
上面就继承了一个abstractMap,也就是用来减轻实现Map接口的编写负担。
2)实现接口
Map<K,V>:封装常用接口
AbstractMap<K, V>:Map接口的 实现类之一,它提供了Map 接口中方法的基本实现。
Cloneable:能够使用Clone()方法,在HashMap中,实现的是浅层次拷贝,即对拷贝对象的改变会影响被拷贝的对象。
Serializable:能够使之序列化,即可以将HashMap对象保存至本地,之后可以恢复状态。
2.2、HashMap类的属性
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,2的30次方。
static final int MAXIMUM_CAPACITY = 1 << 30;
//加载因子,用于扩容使用。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当某个桶节点数量大于8时,会转换为红黑树。
static final int TREEIFY_THRESHOLD = 8;
//当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
static final int UNTREEIFY_THRESHOLD = 6;
//当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。
static final int MIN_TREEIFY_CAPACITY = 64;
//存储元素的数组,transient关键字表示该属性不能被序列化
transient Node<k,v>[] table;
//将数据转换成set的另一种存储形式,这个变量主要用于迭代功能
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器,统计该map修改的次数
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
//临界值,也就是元素数量达到临界值时,会进行扩容。
final float loadFactor;
2.3、HashMap类的构造方法
四个构造方法,构造方法的作用就是记录一下16这个数给threshold(这个数值最终会当作第一次数组的长度。)和初始化加载因子。
注意,hashMap中table数组一开始就已经是个没有长度的数组了。
构造方法中,并没有初始化数组的大小,数组在一开始就已经被创建了。
构造方法只做两件事情:一是初始化加载因子,二是用threshold记录下数组初始化的大小。注意是记录。
1)HashMap()
//看上面的注释就已经知道,DEFAULT_INITIAL_CAPACITY=16,DEFAULT_LOAD_FACTOR=0.75 //初始化容量:也就是初始化数组的大小 //加载因子:数组上的存放数据疏密程度。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 初始化加载因子,所有其他字段默认
}
2)HashMap(int)
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); //初始化加载因子 }
3)HashMap(int, float)
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) //如果初始化容量小于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; this.threshold = tableSizeFor(initialCapacity); }
4)HashMap(Map<? extends K, ? extends V> m)
public HashMap(Map<? extends K, ? extends V> m) {
//初始化填充因子 this.loadFactor = DEFAULT_LOAD_FACTOR;
//将m的所有元素添加到HashMap中 putMapEntries(m, false); }
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) {
//判断table是否初始化 if (table == null) {
//未初始化,s为m的实际个数 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
//计算t大于临界值threshold,则初始化临界值 if (t > threshold) threshold = tableSizeFor(t); } //若已初始化,s大于临界值,则进行扩容 else if (s > threshold) resize();
//将m的所有元素添加到HashMap中 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);//调用添加方法 } } }
2.4、节点源码:链表和红黑树、定位哈希桶数组索引位置
Node节点类源码: // 实现 Map.Entry<K,V> static class Node<K,V> implements Map.Entry<K,V> { final int hash;// 哈希值,在HashMap的存放位置,当HashMap添加元素时,用来与其他元素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; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } // 重写hashCode()方法 public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); //hashCode ^ hashCode 等同于 2^3=8 幂次运算 } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // 重写 equals() 方法 public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
树节点类源码: 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; // 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 TreeNode<K,V> root() { for (TreeNode<K,V> r = this, p;;) { if ((p = r.parent) == null) return r; r = p; }
..........
..........
..........
}
定位哈希桶数组索引位置
不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。
前面说过 HashMap 的数据结构是“数组+链表+红黑树”的结合,所以我们当然希望这个 HashMap 里面的元素位置尽量分布均匀些,
尽量使得每个位置上的元素数量只有一个,那么当我们用 hash 算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表/红黑树,大大优化了查询的效率。
HashMap 定位数组索引位置,直接决定了 hash 方法的离散性能。
下面是定位哈希桶数组的源码: // 代码1 static final int hash(Object key) { // 计算key的hash值 int h; // 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } // 代码2 int n = tab.length; // 将(tab.length - 1) 与 hash值进行&运算 int index = (n - 1) & hash; 整个过程本质上就是三步: 1. 拿到 key 的 hashCode 值 2. 将 hashCode 的高位参与运算,重新计算 hash 值 3. 将计算出来的 hash 值与 (table.length - 1) 进行 & 运算
解读
对于任意给定的对象,只要它的 hashCode() 返回值相同,那么计算得到的 hash 值总是相同的。我们首先想到的就是把 hash 值对 table 长度取模运算,这样一来,元素的分布相对来说是比较均匀的。 但是模运算消耗还是比较大的,我们知道计算机比较快的运算为位运算,因此 JDK 团队对取模运算进行了优化,使用上面代码2的位与运算来代替模运算。这个方法非常巧妙,它通过 “(table.length -1) & h” 来得到该对象的索引位置,
这个优化是基于以下公式:x mod 2^n = x & (2^n - 1)。我们知道 HashMap 底层数组的长度总是 2 的 n 次方,并且取模运算为 “h mod table.length”,对应上面的公式,可以得到该运算等同于“h & (table.length - 1)”。
这是 HashMap 在速度上的优化,因为 & 比 % 具有更高的效率。 在 JDK1.8 的实现中,还优化了高位运算的算法,将 hashCode 的高 16 位与 hashCode 进行异或运算,主要是为了在 table 的 length 较小的时候,让高位也参与运算,并且不会有太大的开销。 下图是一个简单的例子: 当 table 长度为 16 时,table.length - 1 = 15 ,用二进制来看,此时低 4 位全是 1,高 28 位全是 0,与 0 进行 & 运算必然为 0,因此此时 hashCode 与 “table.length - 1” 的 & 运算结果只取决于 hashCode 的低 4 位,
在这种情况下,hashCode 的高 28 位就没有任何作用,并且由于 hash 结果只取决于 hashCode 的低 4 位,hash 冲突的概率也会增加。因此,在 JDK 1.8 中,将高位也参与计算,目的是为了降低 hash 冲突的概率。
2.5、HashMap类的核心方法
2.4.1 get方法
1)get(Object key)
//返回指定键映射到的值 public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
2)Node<K,V> getNode(int hash, Object key)
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// table已经初始化,长度大于0,根据hash寻找table中的项也不为空
// 1 对table进行效验:table不为空 && table长度大于0 && table索引位置(使用table.length-1和hash值进行位与运算)的节点不为空 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// 2 检查first节点的hash值和key 是否和入参的一样,如果一样 则first即为目标节点,直接返回 if (first.hash == hash && // 总是检查first节点 ((k = first.key) == key || (key != null && key.equals(k)))) return first;
// 3 若first不是目标节点,且first的next节点不为空 然后继续遍历 if ((e = first.next) != null) {
//4 判断是否为红黑树结点 if (first instanceof TreeNode)
// 在红黑树中查找 return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 若不是就在链表中查找 do {
//5 执行链表节点的查找,向下遍历链表,直到节点的key,hash和入参的key,hash相等时,返回该节点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } }
//6 找不到 返回空 return null; }
HashMap并没有直接提供getNode接口给用户调用,而是提供的get函数,而get函数就是通过getNode来取得元素的。
如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode,如下
//调用查找根节点 final TreeNode<K,V> getTreeNode(int h, Object k) { // 1.首先找到红黑树的根节点; // 2.使用根节点调用find方法,如果当前不是根节点root()方法的作用就是找出根节点 return ((parent != null) ? root() : this).find(h, k, null); } /** * 从调用此方法的节点开始查找, 通过hash值和key找到对应的节点 * 此方法是红黑树节点的查找, 红黑树是特殊的自平衡二叉查找树 * 平衡二叉查找树的特点:左节点<根节点<右节点 */ final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
// 1. 将p节点赋值为调此方法的节点,即红黑树根节点 TreeNode<K,V> p = this;
//2. 从p节点开始向下遍历 do { int ph, dir; K pk; TreeNode<K,V> pl = p.left, pr = p.right, q;
//3. 如果传入的hash值小于p节点的hash值,则向p节点的左边遍历 if ((ph = p.hash) > h) p = pl; else if (ph < h) //4. 如果传入的hash值大于p节点的hash值,则向p节点的右边遍历 p = pr;
// 5. 如果传入的hash,key值等于p节点的hash,key值,则p节点为目标节点,即返回 else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p; else if (pl == null) //6. p节点的左节点为空则将向右遍历 p = pr; else if (pr == null) //7. p节点的右节点为空则将向左遍历 p = pl;
// 8. 将p节点与key进行比较 else if ((kc != null || (kc = comparableClassFor(k)) != null) && //8.1 kc不为空代表k实现了Comparable (dir = compareComparables(kc, k, pk)) != 0) //8.2 k<pk则dir<0, k>pk则dir>0 // 8.3 k<pk则向左遍历(p赋值为p的左节点), 否则向右遍历
p = (dir < 0) ? pl : pr;
// 9. 代码走到此处, 代表key所属类没有实现Comparable, 直接指定向p的右边遍历 else if ((q = pr.find(h, k, kc)) != null) return q;
//10.代码走到此处代表pr.find(h, k, kc)为空, 因此直接向左遍历 else p = pl; } while (p != null); return null; }
将 p 节点与 k 进行比较。如果传入的 key(即代码中的参数 k)所属的类实现了 Comparable 接口(kc 不为空,comparableClassFor 方法详解如下)
则将 k 跟 p 节点的 key 进行比较(kc 实现了 Comparable 接口,因此通过 kc 的比较方法进行比较),并将比较结果赋值给 dir,
如果 dir<0 则代表 k<pk,则向 p 节点的左边遍历(pl);否则,向 p 节点的右边遍历(pr)。 static Class<?> comparableClassFor(Object x) { // 1.判断x是否实现了Comparable接口 if (x instanceof Comparable) { Class<?> c; Type[] ts, as; Type t; ParameterizedType p; // 2.校验x是否为String类型 if ((c = x.getClass()) == String.class) // bypass checks return c; if ((ts = c.getGenericInterfaces()) != null) { // 3.遍历x实现的所有接口 for (int i = 0; i < ts.length; ++i) { // 4.如果x实现了Comparable接口,则返回x的Class if (((t = ts[i]) instanceof ParameterizedType) && ((p = (ParameterizedType)t).getRawType() == Comparable.class) && (as = p.getActualTypeArguments()) != null && as.length == 1 && as[0] == c) // type arg is c return c; } } } return null; }
2.4.2 put方法
1)put(K key, V value)
//将指定值与此映射中的指定键关联。如果映射之前包含键的映射,则替换旧值。 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
2)putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i;
//1. 效验table是否为空或者length等于0,如果是则调用resize方法进行初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
//2. 通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增节点即可 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
else {
//该索引位置不为空时,则进行查找 Node<K,V> e; K k;
//3. 判断p节点的key和hash值是否跟传入的相等,若相等,则p节点即为要查找的目标节点,将p节点赋值给e节点 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
// 赋值给e,用e来记录 e = p;
//4. 判断节点是否为红黑树结点TreeNode,然后调用红黑树的putTreeVal方法查找目标节点 else if (p instanceof TreeNode)
//放入树中 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//①
// 为链表结点
else {
//5. 走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数 for (int binCount = 0; ; ++binCount) { //6. 如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部 if ((e = p.next) == null) { // 在尾部插入新结点 p.next = newNode(hash, key, value, null); //7. 效验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树。 if (binCount >= TREEIFY_THRESHOLD - 1) // 减一是因为循环是从p节点的下一个节点开始的 treeifyBin(tab, hash);//② // 跳出循环 break; } //8. 如果e节点的hash、key值都与传入的相同,则e节点即为目标节点,跳出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; // 用于遍历桶中的链表,与前面的e = p.next组合,将p指向下一个节点 p = e; } } //9. 如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue if (e != null) { // 记录e的value V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) //用新值替换旧值 e.value = value; afterNodeAccess(e); //用于LinkedHashMap // 返回旧值 return oldValue; } } ++modCount; //10. 实际大小大于临界值则扩容 if (++size > threshold) afterNodeInsertion(evict); return null; }
① 判断节点是否为红黑树结点TreeNode,然后调用红黑树的putTreeVal方法查找目标节点。putTreeVal方法如下
//红黑树的put操作,红黑树插入会同时维护原来的链表属性, 即原来的next属性
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;
//1. 查找根节点,索引位置的头节点并不一定为红黑树的根节点 TreeNode<K,V> root = (parent != null) ? root() : this;
// 2. 将根节点赋值给p节点,开始进行查找 for (TreeNode<K,V> p = root;;) { int dir, ph; K pk;
//3. 如果传入的hash值小于p节点的hash值,将dir赋值为-1,代表向p的左边查找树 if ((ph = p.hash) > h) dir = -1;
//4. 如果传入的hash值大于p节点的hash值,将dir赋值为1,代表向p的右边查找树 else if (ph < h) dir = 1;
//5. 如果传入的hash、key值等于p节点的hash、key值,则p节点即为目标节点,返回p节点 else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p;
//6. 如果k所属的类没有实现Comparable接口 或者 k和p节点的key相等 else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) {
//6.1 第一次符合条件,分别通过p节点的左/右节点来调用find方法进行查找,如果查找到目标节点则返回 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; }
// 6.2 否则使用定义的一套规则来比较k和p节点的key的大小,用来决定向左还是向右查找 dir = tieBreakOrder(k, pk); //③ } TreeNode<K,V> xp = p; //xp赋值为x的父节点,中间变量,用于下面给x的父节点赋值
// 7. dir<=0 则向p左查找,否则向右查找,如果为null,则代表该位置即为x的目标位置 if ((p = (dir <= 0) ? p.left : p.right) == null) {
//走进来代表已经找到x的位置,只需将x放到该位置即可 Node<K,V> xpn = xp.next;
//8. 创建新的节点,其中x的next节点为xpn,即将x节点插入xp与xpn之间 TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
//9. 调整x、xp、xpn之间 的属性关系 if (dir <= 0) //若dir<=0时,则x节点为xp的左节点 xp.left = x; else //若dir>0时,则x节点为xp的右节点 xp.right = x; xp.next = x; //将xp的next节点设置为x x.parent = x.prev = xp; //将x的parent和prev节点设置为xp
//如果xpn不为空,则将xpn的prev节点设置为x节点,与上下文的x节点的next节点对应 if (xpn != null) ((TreeNode<K,V>)xpn).prev = x;
//10. 进行红黑树的插入平衡调整 moveRootToFront(tab, balanceInsertion(root, x)); return null; } } }
②将链表转为红黑树:treeifyBin
//将链表节点转为红黑树节点 final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e;
//1. 如果table为空或者table的长度小于64,则通过resize方法进行扩容 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize();
//2. 根据hash值计算索引值,将该索引位置的节点赋值给e,从e开始遍历该索引位置的链表 else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do {
//3. 将链表节点转红黑树节点 TreeNode<K,V> p = replacementTreeNode(e, null);
//4. 如果是第一次遍历,将头节点赋值给hd if (tl == null) //tl为空代表为第一次循环 hd = p; else {
//5. 如果不是第一次遍历,则处理当前节点的prev属性和上一个节点的next属性 p.prev = tl; //当前节点的prev属性为上一个节点 tl.next = p; //上一个节点的next属性为当前节点 }
//6. 将p节点赋值给tl,用于在下一次循环中作为上一个节点进行一些链表的关联操作(p.prev = tl 和 tl.next = p) tl = p; } while ((e = e.next) != null);
//7. 将table该索引位置赋值为新转的TreeNode的头部节点,如果该节点不为空,则以头节点(hd)为根节点,构建红黑树 if ((tab[index] = hd) != null) hd.treeify(tab);// } }
treeify:构建红黑树 final void treeify(Node<K,V>[] tab) { TreeNode<K,V> root = null;
//1. 将调用此方法的节点赋值给x,以x作为起点,开始进行遍历 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的左/右节点设置为空
//2. 如果还没有根节点,则将x设置为根节点 if (root == null) { x.parent = null; //根节点没有父节点 x.red = false; //根节点必须为黑色 root = x; //将x设置为根节点 } else { K k = x.key; //k赋值为x的key int h = x.hash; //h赋值为x的hash值 Class<?> kc = null;
//3. 如果当前节点x不是根节点,则从根节点开始查找属于该节点的位置 for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key;
//4. 如果x节点的hash值小于p节点的hash值,则将dir赋值为-1,代表向p的左边查找 if ((ph = p.hash) > h) dir = -1;
//5. 如果x节点的hash值大于p节点的hash值,则将dir赋值为1.代表向p的右边查找 else if (ph < h) dir = 1;
//6. 到这代表x的hash 与 p的hash相等。则比较key的大小 else if ((kc == null && //6.1 如果k没有实现Comparable接口 或者 x节点的key和p节点的key相等 (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0)
// 6.2 使用定义的一套规则来比较x节点和p节点的大小,用来决定向左还是向右查找 dir = tieBreakOrder(k, pk); TreeNode<K,V> xp = p; // 将父节点p赋值为xp的父节点,中间变量用于下面给x的父节点赋值
// 7.dir<=0则向p节点左查找,否则向p右边查找,如果为null,则代表该位置即为x的目标位置 if ((p = (dir <= 0) ? p.left : p.right) == null) {
//8. x和xp节点的属性设置 x.parent = xp; // x的父节点即为最后一次遍历的p节点 if (dir <= 0) //如果dir <= 0, 则代表x节点为父节点的左节点 xp.left = x; else //如果dir > 0, 则代表x节点为父节点的右节点 xp.right = x;
//9. 进行红黑树的插入平衡(通过左右旋和改变节点颜色来保证当前树符合红黑树的要求) root = balanceInsertion(root, x); break; } } } }
//10. 如果root节点不在table索引位置的头结点,则将其调整为头节点 moveRootToFront(tab, root); }
moveRootToFront:
1. 将root放到头节点的位置
2. 如果当前索引位置的头节点不是root节点, 则将root的上一个节点和下一个节点进行关联,
3. 将root放到头节点的位置, 原头节点放在root的next节点上
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) { int n; // 1.校验root是否为空、table是否为空、table的length是否大于0 if (root != null && tab != null && (n = tab.length) > 0) { // 2.计算root节点的索引位置 int index = (n - 1) & root.hash; TreeNode<K,V> first = (TreeNode<K,V>)tab[index]; // 3.如果该索引位置的头节点不是root节点,则该索引位置的头节点替换为root节点 if (root != first) { Node<K,V> rn; // 3.1 将该索引位置的头节点赋值为root节点 tab[index] = root; TreeNode<K,V> rp = root.prev; // root节点的上一个节点 // 3.2 和 3.3 两个操作是移除root节点的过程 // 3.2 如果root节点的next节点不为空,则将root节点的next节点的prev属性设置为root节点的prev节点 if ((rn = root.next) != null) ((TreeNode<K,V>)rn).prev = rp; // 3.3 如果root节点的prev节点不为空,则将root节点的prev节点的next属性设置为root节点的next节点 if (rp != null) rp.next = rn; // 3.4 和 3.5 两个操作将first节点接到root节点后面 // 3.4 如果原头节点不为空, 则将原头节点的prev属性设置为root节点 if (first != null) first.prev = root; // 3.5 将root节点的next属性设置为原头节点 root.next = first; // 3.6 root此时已经被放到该位置的头节点位置,因此将prev属性设为空 root.prev = null; } // 4.检查树是否正常 assert checkInvariants(root); } }
// 递归不变量检查 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; }
③tieBreakOrder:比较 k 和 p 节点的 key 的大小,用来决定向左还是向右查找
// 用于不可比较或者hashCode相同时进行比较的方法, 只是一个一致的插入规则,用来维护重定位的等价性。 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; }
2.4.3 resize()方法,扩容方法
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; // 1. 老表的容量不为0,即老表不为空 if (oldCap > 0) { //1.1 判断老表的容量是否超过最大容量值:如果超过则将临界值设置为Integer.MAX_VALUE,直接返回老表。
// 此时oldCap * 2 比Integer.MAX_VALUE大,因此无法重新分布,只是单纯的将临界值扩容到最大 if (oldCap >= MAXIMUM_CAPACITY) { // 临界值为最大整形 threshold = Integer.MAX_VALUE; return oldTab; } // 1.2 将newCap赋值为oldCap的2倍(使用左移(oldCap*2^1),效率更高),若newCap<最大容量并且oldCap>=16,则将临界值设置为原来的2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 临界值翻倍 newThr = oldThr << 1; // double threshold } // 2. 如果老表的容量为0,老表的临界值大于0,是因为初始容量被放入临界值,则将新表的容量设置为老表的临界值 else if (oldThr > 0) newCap = oldThr; else {
//3. 老表的容量为0,老表的临界值为0,这种情况是没有传初始容量的new方法创建的空表,将临界值和容量设置为默认值 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 4. 如果新表的临界值为空,则通过新容量*负载因子获得临界值 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); }
//5. 将当前临界值设置为刚计算出来的新的临界值,定义新表,容量为刚计算出来的新容量,将table设置为新定义的表 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) // 初始化table Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 6. 如果老表不为空,则需遍历所有节点,将节点赋值给新表 if (oldTab != null) { // 复制元素,重新进行hash for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { //将索引值为j的老表头节点赋值给e oldTab[j] = null; //将老表的节点设置为空,以便GC去干活
//7. 如果e.next为空,则代表老表的该位置只有1个节点,计算新表的索引位置,直接将该节点放到该位置 if (e.next == null) newTab[e.hash & (newCap - 1)] = e;
//8. 如果是红黑树节点,则进行红黑树的hash分布(跟链表的hash分布基本相同) else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order
// 9.如果是普通的链表节点,则进行普通的重hash分布 Node<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点 Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引位置+oldCap”的节点 Node<K,V> next; // 将同一桶中的元素根据(e.hash & oldCap)是否为0进行分割,分成两个不同的链表,完成rehash do { next = e.next;
// 9.1 如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样 if ((e.hash & oldCap) == 0) { if (loTail == null) //如果loTail为空,代表该节点为第一个节点 loHead = e; //则将loHead赋值为第一个节点 else loTail.next = e; // 否则将节点添加在loTail后面 loTail = e; // 并将loTail赋值为新增的节点 }
// 9.2 如果e的hash值与老表的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+old else { if (hiTail == null) //如果hiTail为空, 代表该节点为第一个节点 hiHead = e; // 则将hiHead赋值为第一个节点 else hiTail.next = e; // 否则将节点添加在hiTail后面 hiTail = e; //并将hiTail赋值为新增的节点 } } while ((e = next) != null);
// 10.如果loTail不为空(说明老表的数据有分布到新表上“原索引位置”的节点),
// 则将最后一个节点的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点 if (loTail != null) { loTail.next = null; newTab[j] = loHead; }
// 11.如果hiTail不为空(说明老表的数据有分布到新表上“原索引+oldCap位置”的节点),
// 则将最后一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } }
//12. 返回新表 return newTab; }
// 扩容后,红黑树的hash分布,只可能存在于两个位置:原索引位置、原索引位置+oldCap 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; // 存储索引位置为:“原索引位置”的节点 TreeNode<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引+oldCap”的节点 int lc = 0, hc = 0; // 1.以调用此方法的节点开始,遍历整个红黑树节点 for (TreeNode<K,V> e = b, next; e != null; e = next) { // 从b节点开始遍历 next = (TreeNode<K,V>)e.next; // next赋值为e的下个节点 e.next = null; // 同时将老表的节点设置为空,以便垃圾收集器回收 // 2.如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样 if ((e.hash & bit) == 0) { if ((e.prev = loTail) == null) // 如果loTail为空, 代表该节点为第一个节点 loHead = e; // 则将loHead赋值为第一个节点 else loTail.next = e; // 否则将节点添加在loTail后面 loTail = e; // 并将loTail赋值为新增的节点 ++lc; // 统计原索引位置的节点个数 } // 3.如果e的hash值与老表的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+oldCap else { if ((e.prev = hiTail) == null) // 如果hiHead为空, 代表该节点为第一个节点 hiHead = e; // 则将hiHead赋值为第一个节点 else hiTail.next = e; // 否则将节点添加在hiTail后面 hiTail = e; // 并将hiTail赋值为新增的节点 ++hc; // 统计索引位置为原索引+oldCap的节点个数 } } // 4.如果原索引位置的节点不为空 if (loHead != null) { // 原索引位置的节点不为空 // 4.1 如果节点个数<=6个则将红黑树转为链表结构 if (lc <= UNTREEIFY_THRESHOLD) tab[index] = loHead.untreeify(map); else { // 4.2 将原索引位置的节点设置为对应的头节点 tab[index] = loHead; // 4.3 如果hiHead不为空,则代表原来的红黑树(老表的红黑树由于节点被分到两个位置) // 已经被改变, 需要重新构建新的红黑树 if (hiHead != null) // 4.4 以loHead为根节点, 构建新的红黑树 loHead.treeify(tab); } } // 5.如果索引位置为原索引+oldCap的节点不为空 if (hiHead != null) { // 索引位置为原索引+oldCap的节点不为空 // 5.1 如果节点个数<=6个则将红黑树转为链表结构 if (hc <= UNTREEIFY_THRESHOLD) tab[index + bit] = hiHead.untreeify(map); else { // 5.2 将索引位置为原索引+oldCap的节点设置为对应的头节点 tab[index + bit] = hiHead; // 5.3 loHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置) // 已经被改变, 需要重新构建新的红黑树 if (loHead != null) // 5.4 以hiHead为根节点, 构建新的红黑树 hiHead.treeify(tab); } } }
// 将红黑树节点转为链表节点, 当节点<=6个时会被触发 final Node<K,V> untreeify(HashMap<K,V> map) { Node<K,V> hd = null, tl = null; // hd指向头节点, tl指向尾节点 //1.从调用该方法的节点, 即链表的头节点开始遍历, 将所有节点全转为链表节点 for (Node<K,V> q = this; q != null; q = q.next) { //2.调用replacementNode方法构建链表节点 Node<K,V> p = map.replacementNode(q, null); //3.如果tl为null, 则代表当前节点为第一个节点, 将hd赋值为该节点 if (tl == null) hd = p; //4.否则, 将尾节点的next属性设置为当前节点p else tl.next = p; tl = p; //5.每次都将tl节点指向当前节点, 即尾节点 } //6.返回转换后的链表的头节点 return hd; }
remove 方法
//移除某个节点 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; // 1.如果table不为空并且根据hash值计算出来的索引位置不为空, 将该位置的节点赋值给p 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; // 2.如果p的hash值和key都与入参的相同, 则p即为目标节点, 赋值给node if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { // 3.否则将p.next赋值给e,向下遍历节点 // 3.1 如果p是TreeNode则调用红黑树的方法查找节点 if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { // 3.2 否则,进行普通链表节点的查找 do { // 当节点的hash值和key与传入的相同,则该节点即为目标节点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; // 赋值给node, 并跳出循环 break; } p = e; // p节点赋值为本次结束的e,在下一次循环中,e为p的next节点 } while ((e = e.next) != null); // e指向下一个节点 } } // 4.如果node不为空(即根据传入key和hash值查找到目标节点),则进行移除操作 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { // 4.1 如果是TreeNode则调用红黑树的移除方法 if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); // 4.2 如果node是该索引位置的头节点则直接将该索引位置的值赋值为node的next节点, // “node == p”只会出现在node是头节点的时候,如果node不是头节点,则node为p的next节点 else if (node == p) tab[index] = node.next; // 4.3 否则将node的上一个节点的next属性设置为node的next节点, // 即将node节点移除, 将node的上下节点进行关联(链表的移除) else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); // 供LinkedHashMap使用 // 5.返回被移除的节点 return node; } } return null; }
4.1 如果是 TreeNode 则调用红黑树的移除方法:removeTreeNode
这块代码比较长,目的就是移除调用此方法的节点,也就是该方法中的 this 节点。移除包括链表的处理和红黑树的处理。可以结合下文的图解理解。
/** * 红黑树的节点移除 */ final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) { // --- 链表的处理start --- int n; // 1.table为空或者length为0直接返回 if (tab == null || (n = tab.length) == 0) return; // 2.根据hash计算出索引的位置 int index = (n - 1) & hash; // 3.将索引位置的头节点赋值给first和root TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl; // 4.该方法被将要被移除的node(TreeNode)调用, 因此此方法的this为要被移除node节点, // 将node的next节点赋值给succ节点,prev节点赋值给pred节点 TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev; // 5.如果pred节点为空,则代表要被移除的node节点为头节点, // 则将table索引位置的值和first节点的值赋值为succ节点(node的next节点)即可 if (pred == null) tab[index] = first = succ; else // 6.否则将pred节点的next属性设置为succ节点(node的next节点) pred.next = succ; // 7.如果succ节点不为空,则将succ的prev节点设置为pred, 与前面对应 if (succ != null) succ.prev = pred; // 8.如果进行到此first节点为空,则代表该索引位置已经没有节点则直接返回 if (first == null) return; // 9.如果root的父节点不为空, 则将root赋值为根节点 if (root.parent != null) root = root.root(); // 10.通过root节点来判断此红黑树是否太小, 如果是则调用untreeify方法转为链表节点并返回 // (转链表后就无需再进行下面的红黑树处理) if (root == null || root.right == null || (rl = root.left) == null || rl.left == null) { tab[index] = first.untreeify(map); // too small return; } // --- 链表的处理end --- // --- 以下代码为红黑树的处理 --- // 11.将p赋值为要被移除的node节点,pl赋值为p的左节点,pr赋值为p 的右节点 TreeNode<K,V> p = this, pl = left, pr = right, replacement; // 12.如果p的左节点和右节点都不为空时 if (pl != null && pr != null) { // 12.1 将s节点赋值为p的右节点 TreeNode<K,V> s = pr, sl; // 12.2 向左一直查找,跳出循环时,s为没有左节点的节点 while ((sl = s.left) != null) s = sl; // 12.3 交换p节点和s节点的颜色 boolean c = s.red; s.red = p.red; p.red = c; TreeNode<K,V> sr = s.right; // s的右节点 TreeNode<K,V> pp = p.parent; // p的父节点 // --- 第一次调整和第二次调整:将p节点和s节点进行了位置调换 --- // 12.4 第一次调整 // 如果p节点的右节点即为s节点,则将p的父节点赋值为s,将s的右节点赋值为p if (s == pr) { p.parent = s; s.right = p; } else { // 将sp赋值为s的父节点 TreeNode<K,V> sp = s.parent; // 将p的父节点赋值为sp if ((p.parent = sp) != null) { // 如果s节点为sp的左节点,则将sp的左节点赋值为p节点 if (s == sp.left) sp.left = p; // 否则s节点为sp的右节点,则将sp的右节点赋值为p节点 else sp.right = p; } // s的右节点赋值为p节点的右节点 if ((s.right = pr) != null) // 如果pr不为空,则将pr的父节点赋值为s pr.parent = s; } // 12.5 第二次调整 // 将p的左节点赋值为空,pl已经保存了该节点 p.left = null; // 将p节点的右节点赋值为sr,如果sr不为空,则将sr的父节点赋值为p节点 if ((p.right = sr) != null) sr.parent = p; // 将s节点的左节点赋值为pl,如果pl不为空,则将pl的父节点赋值为s节点 if ((s.left = pl) != null) pl.parent = s; // 将s的父节点赋值为p的父节点pp // 如果pp为空,则p节点为root节点, 交换后s成为新的root节点 if ((s.parent = pp) == null) root = s; // 如果p不为root节点, 并且p是pp的左节点,则将pp的左节点赋值为s节点 else if (p == pp.left) pp.left = s; // 如果p不为root节点, 并且p是pp的右节点,则将pp的右节点赋值为s节点 else pp.right = s; // 12.6 寻找replacement节点,用来替换掉p节点 // 12.6.1 如果sr不为空,则replacement节点为sr,因为s没有左节点,所以使用s的右节点来替换p的位置 if (sr != null) replacement = sr; // 12.6.1 如果sr为空,则s为叶子节点,replacement为p本身,只需要将p节点直接去除即可 else replacement = p; } // 13.承接12点的判断,如果p的左节点不为空,右节点为空,replacement节点为p的左节点 else if (pl != null) replacement = pl; // 14.如果p的右节点不为空,左节点为空,replacement节点为p的右节点 else if (pr != null) replacement = pr; // 15.如果p的左右节点都为空, 即p为叶子节点, replacement节点为p节点本身 else replacement = p; // 16.第三次调整:使用replacement节点替换掉p节点的位置,将p节点移除 if (replacement != p) { // 如果p节点不是叶子节点 // 16.1 将p节点的父节点赋值给replacement节点的父节点, 同时赋值给pp节点 TreeNode<K,V> pp = replacement.parent = p.parent; // 16.2 如果p没有父节点, 即p为root节点,则将root节点赋值为replacement节点即可 if (pp == null) root = replacement; // 16.3 如果p不是root节点, 并且p为pp的左节点,则将pp的左节点赋值为替换节点replacement else if (p == pp.left) pp.left = replacement; // 16.4 如果p不是root节点, 并且p为pp的右节点,则将pp的右节点赋值为替换节点replacement else pp.right = replacement; // 16.5 p节点的位置已经被完整的替换为replacement, 将p节点清空, 以便垃圾收集器回收 p.left = p.right = p.parent = null; } // 17.如果p节点不为红色则进行红黑树删除平衡调整 // (如果删除的节点是红色则不会破坏红黑树的平衡无需调整) TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement); // 18.如果p节点为叶子节点, 则简单的将p节点去除即可 if (replacement == p) { TreeNode<K,V> pp = p.parent; // 18.1 将p的parent属性设置为空 p.parent = null; if (pp != null) { // 18.2 如果p节点为父节点的左节点,则将父节点的左节点赋值为空 if (p == pp.left) pp.left = null; // 18.3 如果p节点为父节点的右节点, 则将父节点的右节点赋值为空 else if (p == pp.right) pp.right = null; } } if (movable) // 19.将root节点移到索引位置的头节点 moveRootToFront(tab, r); }
具体详情解释,敬请期待。
HashMap 和 Hashtable 的区别 1. HashMap 允许 key 和 value 为 null,Hashtable 不允许。 2. HashMap 的默认初始容量为 16,Hashtable 为 11。 3. HashMap 的扩容为原来的 2 倍,Hashtable 的扩容为原来的 2 倍加 1。 4. HashMap 是非线程安全的,Hashtable是线程安全的。 5. HashMap 的 hash 值重新计算过,Hashtable 直接使用 hashCode。 6. HashMap 去掉了 Hashtable 中的 contains 方法。 7. HashMap 继承自 AbstractMap 类,Hashtable 继承自 Dictionary 类。
总结 1. HashMap 的底层是个 Node 数组(Node<K,V>[] table),在数组的具体索引位置,如果存在多个节点,则可能是以链表或红黑树的形式存在。 2. 增加、删除、查找键值对时,定位到哈希桶数组的位置是很关键的一步,源码中是通过下面3个操作来完成这一步:
1)拿到 key 的 hashCode 值;
2)将 hashCode 的高位参与运算,重新计算 hash 值;
3)将计算出来的 hash 值与 “table.length - 1” 进行 & 运算。 3. HashMap 的默认初始容量(capacity)是 16,capacity 必须为 2 的幂次方;默认负载因子(load factor)是 0.75;实际能存放的节点个数(threshold,即触发扩容的阈值)= capacity * load factor。 4. HashMap 在触发扩容后,阈值会变为原来的 2 倍,并且会对所有节点进行重 hash 分布,重 hash 分布后节点的新分布位置只可能有两个:“原索引位置” 或 “原索引+oldCap位置”。
例如 capacity 为16,索引位置 5 的节点扩容后,只可能分布在新表 “索引位置5” 和 “索引位置21(5+16)”。 5. 导致 HashMap 扩容后,同一个索引位置的节点重 hash 最多分布在两个位置的根本原因是:
1)table的长度始终为 2 的 n 次方;
2)索引位置的计算方法为 “(table.length - 1) & hash”。HashMap 扩容是一个比较耗时的操作,定义 HashMap 时尽量给个接近的初始容量值。 6. HashMap 有 threshold 属性和 loadFactor 属性,但是没有 capacity 属性。
初始化时,如果传了初始化容量值,该值是存在 threshold 变量,并且 Node 数组是在第一次 put 时才会进行初始化,初始化时会将此时的 threshold 值作为新表的 capacity 值,然后用 capacity 和 loadFactor 计算新表的真正 threshold 值。 7. 当同一个索引位置的节点在增加后达到 9 个时,并且此时数组的长度大于等于 64,则会触发链表节点(Node)转红黑树节点(TreeNode),转成红黑树节点后,其实链表的结构还存在,通过 next 属性维持。
链表节点转红黑树节点的具体方法为源码中的 treeifyBin 方法。而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容。 8. 当同一个索引位置的节点在移除后达到 6 个时,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点。红黑树节点转链表节点的具体方法为源码中的 untreeify 方法。 9. HashMap 在 JDK 1.8 之后不再有死循环的问题,JDK 1.8 之前存在死循环的根本原因是在扩容后同一索引位置的节点顺序会反掉。 10. HashMap 是非线程安全的,在并发场景下使用 ConcurrentHashMap 来代替。
参考原文:https://blog.csdn.net/v123411739/article/details/78996181