参考笔记:java TreeSet 和 TreeMap 源码解读-CSDN博客
目录
4. TreeSet源码解读:演示第一种排序机制:元素类实现Comparable接口
5. TreeSet源码解读:演示第二种排序机制:提供Comparator比较器
1.前言
本文将通过 Debug 流程分析,剖析 TreeSet 以及 TreeMap 的底层实现。TreeSet 的底层基于 TreeMap 实现,而 TreeMap 使用 红黑树 Red-Balck Tree 作为数据结构。因此本文在对 TreeSet、TreeMap 源码分析之前会先介绍一下红黑树的基本知识
注意:本篇博文对 HashSet 源码的解读基于主流的 JDK 8.0 的版本
2.红黑树
红黑树(Red-Black Tree)是一种自平衡二叉搜索树,它通过特定的颜色规则和调整操作来保持树的平衡
2.1 红黑树的五大性质
① 颜色属性:每个节点要么是红色,要么是黑色
② 根节点属性:根节点必须是黑色
③ 红色节点属性:红色节点的子节点必须是黑色,即不能有两个连续的红色节点
④ 叶子节点属性:所有叶子节点(NIL节点,空节点)都是黑色
⑤ 黑高一致性:从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点,称为"黑高"
补充:红黑树的叶子节点与普通二叉树叶子节点的区别
普通二叉树:没有子节点的节点为叶子节点
红黑树:所有实际数据节点都被视为内部节点,真正的叶子节点是虚拟的NIL/NULL节点
那为什么红黑树的叶子节点要这样设计呢?
简化边界条件处理:将所有空指针都视为统一的 NIL 节点
保持规则一致性:
确保"从节点到叶子路径的黑高相同"规则可以统一应用
确保"红色节点的子节点必须是黑色"规则可以统一检查
代码实现方便
2.2 节点颜色的初始设置
新插入的节点:默认总是设置为红色
原因:设置为红色不会违反红黑树的性质⑤:黑高一致性
风险:可能会违反"无连续红色节点"规则(性质③)
特殊情况:如果插入的是根节点,会强制变为黑色(满足性质②)
2.3 插入新节后的调整
插入的新节点初始是红色,也就是 "红插" ,后续将根据平衡性变色和调整,规则如下:
① 新节点的父节点是黑色:不需要任何调整,没有违反红黑树的五大性质
② 新节点的父节点是红色:
需要根据叔叔节点(父节点的兄弟)的颜色进一步处理:
叔叔节点是黑色:
LL型:右单旋,父换爷+变色
RR型:左单旋,父换爷+变色
LR型:左右双旋,儿换爷+变色
RL型:右左双旋,儿换爷+变色
叔叔节点是红色:
叔父爷节点先全部变色,爷爷节点变成新节点,然后再来一轮红黑树调整
关于红黑树的旋转、调整等其他操作大家可以去在线演示网站试试,有助于理解,链接如下:
Red/Black Tree Visualization
https://www.cs.usfca.edu/~galles/visualization/RedBlack.html
2.4 删除结构后的调整
删除节点后可能会破坏黑高一致性,需要通过复杂的旋转和重新着色来恢复平衡,本文不作探讨
2.5 排序规则
left < root < right
左子树规则:左子树中的所有结点都小于根节点
右子树规则:右子树中的所有结点都大于根节点
2.6 设计红黑树的原因
🆗,下面开始本文的正式内容,TreeSet、TreeMap的源码解析
3.TreeSet简介、底层实现
3.1 TreeSet简介
① TreeSet 是单列集合 Set 接口的一个实现类,位于 java.util.TreeSet 下,其类定义和继承关系图如下所示:
② TreeSet 中的元素不能为 null ,否则会报 NullPointerException
③ 与 HashSet 类不同, TreeSet 最大的特点是可以进行排序。TreeSet 底层基于 TreeMap,而 TreeMap 底层由红黑树实现,可以对元素对象进行排序
排序机制要求:
要么元素类实现 Comparable 接口,重写 compareTo 方法,自定义排序逻辑
要么在创建 TreeSet 集合时提供 Comparator 比较器并重写 compare 方法,自定义排序逻辑
3.2 TreeSet底层实现
① TreeSet 的底层是 TreeMap ,而 TreeMap 是使用红黑树数据结构实现的
② TreeSet 中的每个结点类型是 TreeMap$Entry ,Entry 是 TreeMap 的一个静态内部类,其类的定义如下:
③ TreeSet 的去重机制:
(1)如果使用 TreeSet 的无参构建创建 TreeSet 集合,并且添加的元素类已经实现了Comparable 接口的 compareTo 方法,则使用 compareTo 方法去重。即如果 compareTo 方法返回值为 0 ,则认为是相同的元素或数据,放弃添加
(2)如果使用 TreeSet 的有参构造创建 TreeSet 集合时提供了 Comparator 比较器,就使用重写的 compare 方法进行去重。即如果 compare 方法返回值为 0 ,则认为是相同的元素或数据,放弃添加
注意:如果既没有在创建 TreeSet 对象时提供 Comparator 比较器,也没有在所添加的元素的类中重写 Comparable 接口的 compareTo 方法,那么在添加元素时 JVM 一定会报类型转换异常 ClassCastException
示例代码:
import java.util.TreeSet; public class demo { public static void main(String[] args) { System.out.println("hello world"); TreeSet treeSet = new TreeSet(); treeSet.add(new Cat()); } } class Cat { //none }
运行结果:
④ TreeSet 的底层是 TreeMap ,那肯定会需要频繁访问 TreeMap 中的属性或者方法,那 Java 中是如何实现的呢?
TreeSet 类中维护了一个属性 private transient NavigableMap<E,object> m,初始值为 null ,TreeSet 就是用 m 来访问 TreeMap 中的属性或方法的,其源码如下:
但 NavigableMap 是一个接口类型,看到这大家肯定会觉得懵逼,前面不是说要用 m 来访问 TreeMap 吗? 这类型都不一致,怎么访问啊?别急,我们来看看 TreeMap 和 NavigableSet 的关系,如下图:
🆗,可以看到,TreeMap 类 实现了 NavigableMap 接口,所以接口 NavigableMap 引用可以指向 TreeMap 对象,只能说多态机制牛逼!!!TreeSet 底层正好就是这么做的,在使用 TreeSet 构造器创建 TreeSet 集合对象时,会给 private transient NavigableMap<E,object> m 作初始化,下面我以无参构造器为例:
所以 private transient NavigableMap<E,object> m 最终的运行类型就是 TreeMap,这样TreeSet 对象内部就可以通过这个 m 去访问 TreeMap 中的属性或者方法了。 还是想感叹 Java 的多态机制真牛逼!!!
4. TreeSet源码解读:演示第一种排序机制:元素类实现Comparable接口
4.1 准备工作
我们这里的案例是向 TreeSet 集合中添加 int 类型,但底层会实现自动装箱 int ---> Integer。而 Integer 类刚好实现了 Comparable 接口,并实现 CompareTo 方法,如下:
Integer 实现的 compareTo 方法内部调用了 compare 方法。compareTo 方法在底层会作为该红黑树的排序机制和去重机制
最终该红黑树的排序规则如下:
left < root < right
最终打印 TreeSet 集合的效果就是按升序排序
去重机制:当前添加的元素与某个结点使用 compareTo 方法比较时,如果返回值 = 0,则为重复元素,放弃添加
🆗,解释完毕后,将用以下代码作为演示类,一步一步 Debug
import java.util.TreeSet;
public class demo {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet();
treeSet.add(100);
treeSet.add(3);
treeSet.add(3);//重复元素,放弃添加
treeSet.add(50);
treeSet.add(44);
System.out.println("treeSet = " + treeSet);//[3,44,50,100]
}
}
4.2 跳入TreeSet无参构造器
首先跳入 TreeSet 的带参构造,如下 :
我们先看上图中的 ① ,调用的是 TreeMap 的无参构造,无参构造内有一个赋值语句 comparator = null,comparator 是 TreeMap 类维护的一个属性,用来接收传入的 Comparator 比较器,作为 TreeSet 集合的排序机制和去重机制。其源码如下:
由于我们并没有传入 Comparator 比较器,所以 comparator = null
接着 ② 则是调用 TreeSet 的一个有参构造器 TreeSet(NavigableMap<E,Object> m) ,有参构造器内也是一个赋值于语句:this.m = m,为 TreeSet 的 m 属性赋值。这里的 m 我们在前文的 "TreeSet底层实现" 中提到过,TreeSet 就是通过 m 来访问 TreeMap 中的属性和方法的,其源码如下:
OK,我们跳出 TreeSet 无参构造器,此时的 TreeSet 集合状态如下:
可以看到,正如我们在 "TreeSet底层实现" 所说的,m 的类型确实是 TreeMap,TreeMap 有挺多属性的,但我们只需要关注上图中绿色框部分。由于当前集合未添加元素,所以此时的 root = null,size = 0,modCount = 0
4.3 向集合中添加第一个元素100
① 跳入add 方法
向 TreeSet 集合中添加第一个元素 100 ,跳入 add 方法,这里会先进入 valueOf 方法进行装箱操作 int ---> Integer,我们直接跳出,直接重新进入 add 方法,如下所示 :
可以看到,底层走的是 TreeMap 的 put 方法;注意实参中的 PRESENT, 这个PRESENT 和 HashSet 中 PRESENT 是一样的,只起到占位的作用,其源码如下 :
② 跳入put方法
继续往下执行,跳入 put 方法,其 源码+注释 如下所示,非常详细:
//添加元素到treeSet集合中
//key:元素值 value:PRESENT占位符
public V put(K key, V value) {
//记录红黑树的根结点
TreeMap.Entry<K,V> t = root;
/** 集合元素个数 = 0 ,执行 if 语句 **/
//添加第一个元素时,t = null,会执行该 if 语句
if (t == null) {
compare(key, key); //健壮性判断,这一行可以不用管
//创建一个新结点作为根结点
root = new TreeMap.Entry<>(key, value, null);
size = 1;//集合元素+1
modCount++;//集合修改次数+1
return null;//返回null表示添加成功
}
/** 集合中元素个数 >= 1,执行以下代码 **/
int cmp;//记录元素比较结果
TreeMap.Entry<K,V> parent;
//将comparator属性赋值给cpr,comparator为比较器
Comparator<? super K> cpr = comparator;
//如果在创建TreeSet集合对象时传入了比较器,则cpr必不为空,会执行该if语句
if (cpr != null) {
/*
do while循环的作用:利用比较器cpr中的compare方法作为作为排序逻辑,
依次遍历红黑树,判断当前添加的元素是否重复:
(1)如果不重复,则为当前添加的元素找到合适的存放位置,最终parent即为添加元素的父结点
(2)如果重复,则放弃添加
*/
do {
parent = t;
cmp = cpr.compare(key, t.key);//比较欲添加的元素和当前结点t的key
if (cmp < 0)
t = t.left;//往左子树查找存放位置
else if (cmp > 0)
t = t.right;//右右子树查找存放位置
else//如果cmp = 0 ,说明添加的元素与当前结点t重复,放弃添加
return t.setValue(value);
} while (t != null);
}
//如果在元素类中实现了Comparable接口的compareTo方法,则会执行该else语句
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
/*
do while循环的作用:利用元素类中实现的Comparable接口的compareTo方法
作为排序逻辑,依次遍历红黑树,判断当前添加的元素是否重复
(1)如果不重复,则为当前添加的元素找到合适的存放位置,最终parent即为添加元素的父结点
(2)如果重复,则放弃添加
*/
do {
parent = t;
cmp = k.compareTo(t.key);//比较欲添加的元素和当前结点t的key
if (cmp < 0)
t = t.left;//往左子树查找存放位置
else if (cmp > 0)
t = t.right;//往右子树查找存放位置
else//如果cmp = 0 ,说明添加的元素与当前结点t重复,放弃添加
return t.setValue(value);
} while (t != null);
}
/**若不是第一次往TreeSet中添加元素,且能执行到这里,说明为当前欲添加的元素不重复,为其找到了合适的存放位置**/
//将添加的元素包装在Entry类中,记录key:添加元素,value:PRESENT占位符,parent:父结点
TreeMap.Entry<K,V> e = new TreeMap.Entry<>(key, value, parent);
//根据与父结点的比较结果cmp来决定存放在父节点的左子树还是右子树
if (cmp < 0)
parent.left = e;
else
parent.right = e;
//插入新结点后,需要调用fixAfterInsertion函数来完成红黑树中结点的颜色调整、旋转等
fixAfterInsertion(e);
//更新size:集合中的元素个数 modCount:修改集合的次数
size++;
modCount++;
//添加成功,返回null
return null;
}
这里我还是一步一步 Debug 过一遍,等整个流程搞清楚再看上面的 源码+注释 会更容易理解一点
我们是向 TreeSet 集合中添加第一个元素 100,所以此时的 root = null ,只会执行上图中的红框部分,即 if 结构
if 结构中的第一行 compare 方法不需要理会,它只是作一个健壮性的判断。接下来就是root = new Entry<>(key,value,null),即将当前添加的元素用 Entry 类包装起来,这里我们跳进去看看该构造器方法,如下:
可以看到,构造器中对 Entry 类的 key、value、parent 的属性进行赋值,key(存放添加元素):100;value:PRESENT占位符,无实际意义;parent(父结点):null;还有一个比较关键的地方就是 color = BLACK(由红黑树的五大性质可知,根节点是黑结点)
OK,重新回到 put 方法,继续往下执行,更新集合元素个数 size = 1 ,更新集合修改次数 moCount ++
最后 return null 表示添加元素成功,跳出 put 方法
③ 逐层返回,跳回演示类中
逐层返回,回到演示类中,第一个元素 100 添加成功,查看此时的 TreeSet 集合的状态,如下:
可以看到,第一个元素 100 添加成功,且作为根节点
4.4 向集合中添加第二个元素 3
① 跳入put方法
对于第二个元素 3 的添加,我们直接跳到 put 方法,如下 :
首先,由于此时 root != null,所以 t != null,不会执行第一个 if 语句
接着,是两个变量的定义:
定义了一个 int 型变量 cmp 和 Entry 类型变量 parent。这里先不作解释,后面用到再说
继续,往下执行, 如下所示:
由于我们并没有在创建 TreeSet 集合对象时传入比较 Comparator ,所以 comparator = cpr = null,所以不会进入该 if 结构中
接着,重点的来了,如下图所示:
首先,先判断添加的元素 key 是否为 null ,如果为 null 则抛出空指针异常 NullPointerException
接着,进入非常关键的 do while 循环,do while 循环的作用如下:
利用元素类 Integer 中实现的 Comparable 接口的 compareTo 方法作为排序机制和去重机制,依次遍历红黑树,判断当前添加的元素是否重复:
(1)如果不重复:则为当前添加的元素找到合适的存放位置,最终 parent 即为添加元素的父节点
(2)如果重复:放弃添加
我们当前添加的元素为 3 ,显然不会重复,经过 do while 循环,最终 parent 是元素 3 的父结点,我们看一下元素 3 的父节点是谁:
可以看到,元素 3 的父节点是我们刚才添加的第一个元素 100
前面的代码只是为元素 3 找到了合适的存放位置,即找到了元素 3 的父节点,但还未真正将元素 3 加入到集合中,所以继续往下执行,如下所示:
第一行即将当前添加的元素 3 用 Entry 类包装起来,调用的构造器我们在前面也有提到过,这里就不再赘述了。重点是下面的 if --- else 结构,下面的 if---else 结构的作用:根据当前添加的元素与其父节点 parent 的比较结果 cmp 来决定存放在父节点的左子树还是右子树。由于当前添加的元素是 3 ,父节点 parent 是 100,因此 cmp 返回值为 -1 ,执行 if 语句,如下所示:
所以,元素 3 存放在 100 的左结点
OK,到这别忘了,因为我们在红黑树中插入了一个新结点,而红黑树又要遵守其五大性质,所以可能需要进行红黑树结点颜色的调整、旋转等,那源码中如何实现的呢?
很简单,Java 是将红黑树结点颜色的调整、旋转等问题包装成一个叫做 fixAfterInsertion函数,如下:
见名知意,fixAfterInsertion 的意思就是 "修复插入之后的问题" 。可以看到, fixAfterInsertion 方法的第一行 x.color = RED 就是将新插入的结点颜色设置为红色,之后再通过一个 while 循环判断是否需要对红黑树的结点颜色进行调整和旋转,源码比较复杂,感兴趣的朋友可以自己看一下
最后,执行完 fixAfterInsertion 函数就是更新集合元素个数 size 、更新集合修改次数modCount,return null 表示添加元素成功
② 逐层返回,跳回演示类中
执行完 put 方法后,逐层返回,回到演示类,第二个元素 3 添加成功,查看此时的 TreeSet 集合的状态,如下:
可以看到,添加的元素 3 确实存放在元素 100 的左节点 left ,此时的红黑树结构如下图所示:
4.5 向集合中添加重复元素3
当前我们演示的是第一种排序机制:元素类实现 Comparable 接口,所以 TreeSet 的去重机制是根据实现的 Comparable 接口的 compareTo 方法来实现的。即两个元素利用 compareTo 方法进行比较时,如果返回值 = 0,说明元素重复,放弃添加
① 跳入put方法
同样地,前面一些相同的步骤我们不再赘述,我们直接跳到 put 方法中判断重复元素的位置,如下:
do while 循环的作用我们在上文中已经说明,忘记了可以往前翻一下。这里由于当前添加的元素 3 为重复元素,所以一定会进入 do while 循环中的 else 部分,如下:
可以看到,else 部分执行的语句是 return t.setValue(value) ,setValue 方法中其实就是将重复结点 t 的 value(PRESENT占位符)重复赋值为PRESENT占位符,其实相当于什么都没做,然后 return oldValue = return PRESENT ,所以 put 方法如果添加的是重复元素会 return t.setValue(value) = return PRESENT
② 回到add方法
put 方法执行完毕,回到 add 方法,由于添加的是重复元素,所以 add 方法的返回值一定为 false ,如下所示:
③ 回到演示类中
add 方法执行完毕,回到演示类中,重复元素 3 放弃添加,查看此时的 TreeSet 集合的状态,如下:
可以看到,集合元素个数 size = 2,元素更改次数 modCount = 2,说明重复元素 3 确实没有加入到 TreeSet 集合中
4.6 继续向集合添加元素
继续添加元素 50、44,由于排序机制、查找元素存放位置、去重机制前面已经讲的很详细了,所以这里直接看添加元素 55、44 之后的集合状态,如下所示:
可以看到,根节点 root 变成了元素 50 ,集合元素个数 size = 4,集合更改次数 modCount = 4,此时的红黑树结构如下所示:
每个结点满足 left < root < right
4.7 输出结果
treeSet 集合输出结果如下:
可以看到,System.out.print(treeSet) 输出的是 treeSet 集合的升序排序结果
🆗, TreeSet 第一种排序机制的源码分析完毕,下面进行 TreeSet 第二种排序机制的源码分析
5. TreeSet源码解读:演示第二种排序机制:提供Comparator比较器
5.1 准备工作
我们这里的案例仍然是向 TreeSet 集合中添加 int 类型,底层会实现自动装箱 int ---> Integer。但不再使用 Integer 类实现的 Comparable 接口的 compareTo 方法,而是给 TreeSet 传入一个 Comparator 比较器,在比较器中重写 compare 方法,作为 TreeSet 的排序机制和去重机制
🆗,将用以下代码作为演示类,一步一步 Debug
import java.util.Comparator;
import java.util.TreeSet;
public class demo {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet(new Comparator<Integer>() {
@Override
public int compare(Integer x, Integer y) {//该比较器可以实现一个降序排序的效果
if (x < y){
return 1;
}else if (x > y){
return -1;
}else{
return 0;
}
//三目运算符的写法:return (x < y) ? 1 : ((x == y) ? 0 : -1);
}
});
treeSet.add(100);
treeSet.add(3);
treeSet.add(3);//重复元素,放弃添加
treeSet.add(50);
treeSet.add(44);
System.out.println("treeSet = " + treeSet);//[100,50,44,3]
}
}
为了更好的与第一种排序机制比较,演示代码向 TreeSet 集合中添加的是元素同样是 100、3、3、50、44,不同之处在于我们提供的 Comparator 比较器实现的是降序排序
该红黑树的排序规则如下:
left > root > right
去重机制:当前添加的元素与某个结点使用 Comparator 比较器中重写的 compare 方法比较时,如果返回值 = 0,则为重复元素,放弃添加
最终 System.out.println("treeSet = " + treeSet) 的输出结果是:treeSet = [100,50,44,3]
5.2 跳入TreeSet有参构造器
首先我们跳入 TreeSet 的带参构造,如下 :
先不着急跳入 TreeMap 的构造器,先看一下 TreeSet 带参构造的形参:是一个 Comparator(比较器)类型的变量,这个 Comparator 类型其实就是一个接口,其源码如下 :
而我们一开始在演示类中传入的是这个玩意:
new Comparator<Integer>() {
@Override
public int compare(Integer x, Integer y) {//该比较器可以实现一个降序排序的效果
if (x < y){
return 1;
}else if (x > y){
return -1;
}else{
return 0;
}
//三目运算符的写法:return (x < y) ? 1 : ((x == y) ? 0 : -1);
}
}
其实就是一个实现了 Comparator 接口的匿名内部类对象,并在 TreeSet 带参构造中形成了多态。而在 TreeSet 带参构造中,又将匿名内部类对象 comparator 传递给了 TreeMap 的一个带参构造
匿名内部类中实现了 Comparator 接口中的 compare 方法,该方法决定了 TreeSet 集合的排序机制和去重机制
🆗,解释清楚了,我们重新回到下面这张图:
看 ① ,追入 TreeMap 的带参构造器,如下图所示:
可以看到,在 TreeMap 构造器中,将匿名内部类对象 comparator 传递给了 TreeMap 的一个属性 comparator。并且,此时的 comparator 已经显示为了匿名内部类 "demo$1" 的形式
接着看 ② ,仍然是将 new 好的 TreeMap 对象赋值给 TreeSet 的 m 属性
5.3 向集合添加第一个元素 100
向集合中添加第一个元素 100 的流程步骤和演示第一种排序机制的时候是完全一致的,所以这里我以 GIF 展示,如下所示:
可以看到,第一个元素 100 成功添加到 TreeSet 集合中,且作为 root 根结点
5.4 向集合添加第二个元素3
① 跳入put方法
我们直接跳到 put 方法,如下所示:
这时候 t 显然不为空,不会进入第一个 if 语句
继续往下执行,重点来了,如下:
仔细观察上图中的红色底线部分,底层是利用匿名内部类中重写的 compare 方法来比较欲添加的元素和当前结点 t 的 key 。根据动态绑定机制,这时候如果我们跳入 compare 方法,一定会跳到演示类中的匿名内部类, 如下图所示 :
所以 TreeSet 确实是根据匿名内部类中重写的 compare 方法作为排序机制和去重机制
好的,多的也不瞎扯了,执行完 compare 方法,回到原来的 if 结构中:
其实 该 if 结构中的代码本文在 "4. TreeSet源码解读:演示第一种排序机制:元素类实现Comparable接口" 部分已经非常详细的讲解过了,只有 cmp = cpr.compare(key,t.key) 这一条语句的差别,大家可以往前翻一下,这里就不再赘述了、
继续往下执行,如下:
"老演员"了
首先,将添加的元素 3 用 Entry 包装起来;后面的 if--else 结构根据 cmp 值决定元素 3 存放在父节点 parent 的左结点还是右结点;接着,因为我们在红黑树中插入了一个新结点,所以可能需要进行红黑树结点颜色的调整、旋转等,因此调用 fixAfterInsertion(e) 函数,函数名称见名知意:"修复插入的问题"。最后就是更新集合的元素个数 size、更新集合修改次数 modCount、return null 表示添加元素成功
put 方法执行结束
② 逐层返回,跳回演示类中
执行完 put 方法后,逐层返回,回到演示类,第二个元素 3 添加成功,查看此时的TreeSet 集合的状态,如下:
可以看到,添加的元素 3 存放在元素 100 的右节点 right ,此时的红黑树结构如下所示:
5.5 继续向集合添加元素
继续向集合中添加元素 3(重复元素,放弃添加)、50、44,添加之后的 TreeSet 集合状态如下:
可以看到,根节点 root 变成了元素 50 ,集合元素个数 size = 4,集合更改次数 modCount = 4,此时的红黑树结构如下所示:
每个结点满足 right > root > left
5.6 输出结果
treeSet 集合输出结果如下:
可以看到,System.out.print(treeSet) 输出的是 treeSet 集合的降序排序结果
🆗,TreeSet 第二种排序机制的源码分析完毕,下面进行 TreeMap 的源码分析
6.TreeMap简介+底层实现
说明:TreeMap 的底层实现大多在上面的 TreeSet 部分基本已经讲完了
① TreeMap作为 Map 接口(双列集合)的一个实现类,也是保存和记录 key-value 的映射关系——键值对的,其类定义与继承关系图如下:
② 同 TreeSet 集合一样,TreeMap 中的每个结点类型是 TreeMap$Entry ,Entry 是 TreeMap 的一个静态内部类,其类的定义如下:
注意,对于 TreeSet 来说,每个结点的 value 值是 PRESENT占位符,无实际意义。而 TreeMap 中每个 value 值存储的是一个有意义的值,是 key-value 键值对中的 value
② TreeMap 中,key 不可以为 null ,否则会报 NullPointerException ;但是 value 可以为 null
③ TreeMap 也可以对元素进行自定义排序,实现方式与 TreeSet 一致
排序机制要求:
要么元素类实现 Comparable 接口,重写 compareTo 方法,自定义排序逻辑
要么在创建 TreeMap 集合时提供 Comparator 比较器并重写 compare 方法,自定义排序逻辑
不过需要注意的是,是根据 key-value 键值对中的 key 进行排序的,至于为什么是根据 key 排序,看上文 TreeSet 的源码解读就知道了,关键代码在 TreeMap 类中的 put 方法,如下:
可以看到,两种排序机制比较的都是 key-value 键值对中的 key
④ TreeMap 集合中,如果先添加键值对 {key:1,value:"小明"} ,再添加键值对 {key:1,value:"蔡徐坤"} ,那么 key = 1 对应的 value 会被覆盖为 "蔡徐坤" 。本质上其实就是 TreeMap 集合不允许 key 重复,判断 key 重复的机制与 TreeSet 是一样的,如下:
(1)如果使用 TreeMap 的无参构建创建 TreeMap 集合,并且添加的元素类已经实现了Comparable 接口的 compareTo 方法,则使用 compareTo 方法去重。即如果 compareTo 方法返回值为 0 ,则认为是相同的 key ,用新的 value 覆盖旧的 value
(2)如果使用 TreeMap 的有参构造创建 TreeMap 集合时提供了 Comparator 比较器,就使用重写的 compare 方法进行去重。即如果 compareTo 方法返回值为 0 ,则认为是相同的 key ,用新的 value 覆盖旧的 value
实现代码其实在 TreeSet 源码解读部分讲解过了,关键代码在 TreeMap 类中的 put 方法,如下:
7.TreeMap源码解读
由于流程和 TreeSet 是基本一致的,所以此处就简单过一下,点到为止
7.1 准备工作
使用的案例中,键值对 key-value 类型是 Integer-String 。但不再使用 Integer 类实现的Comparable接口的 compareTo 方法,而是在创建 TreeMap 集合时传入一个 Comparator 比较器,在比较器中重写 compare 方法,作为 TreeMap 的排序机制和判断重复逻辑
🆗,将用以下代码作为演示类
import java.util.Comparator;
import java.util.TreeMap;
public class demo {
public static void main(String[] args) {
TreeMap<Integer, String> treeMap = new TreeMap<>(new Comparator<Integer>() {
@Override
public int compare(Integer x, Integer y) {//按key降序排序,若返回值为0,说明key重复
return (x < y) ? 1 : ((x == y) ? 0 : -1);
}
});
treeMap.put(23,"鸡你太美");
treeMap.put(50,"Jack");
treeMap.put(111,"小马");
treeMap.put(111,"蔡徐坤");//key = 111 重复,用"蔡徐坤"覆盖"小马"
System.out.println("按 key 进行降序排序,结果如下:");
for (Integer key : treeMap.keySet()) {
String value = treeMap.get(key);
System.out.println(key + " = " + value);
}
}
}
我们提供的 Comparator 比较器实现的是按 key 降序排序
所以该红黑树的排序规则如下:
left > root > right
判断重复逻辑:当前添加的 key-value 的 key 与某个结点的 key 使用 Comparator 比较器中重写的 compare 方法比较时,如果返回值 = 0,则为重复,用新的 vaue 覆盖旧 value
7.2 TreeMap构造器
同样地,先跳入 TreeMap 的构造器看看,如下 :
还是那老一套,将实现了 Comparator 接口的匿名内部类对象传递给 TreeMap 的 comparator属性
7.3 向集合添加第一个元素
向集合添加第一个元素 {key:23,value:"鸡你太美"} ,跳入 put 方法,如下 :
可以看到,此时的 value 值已经不是PRESENT占位符,而是传入的实参 "鸡你太美"
由于是第一个添加的元素,所以此时 t = root = null,因此会执行 if 结构中的代码。所以说和 TreeSet 基本就一套,没啥讲得😂
7.4 向集合添加第二个元素
向集合添加第二个元素 {key:50,value:"Jack"} ,仍然是跳入 put 方法,我们直接定位到关键部分,如下:
这时候,我们跳入 compare 方法,根据动态绑定机制,一定会跳到演示类中的匿名内部类中,如下图所示 :
OK,接下来还是会根据 compare 方法的返回值进行判断,执行对应的添加操作。这里就不再演示了,我们将剩余的元素 {key:111,value:"小马"} 、{key:111,value:"蔡徐坤"} (key重复)添加进去,如下图所示 :
可以看到, 3 个键值对成功添加到 TreeMap 集合中,元素个数 size= 3,修改集合次数modCount = 3(在 key 重复时,用新 value 值覆盖旧 value 值,modCount 不更新)
7.5 输出结果
TreeMap 集合输出结果如下:
可以看到,TreeMap 集合输出的结果确实是按 key 值降序排序的,而且后添加的键值对{key:111,"蔡徐坤"} 的 "蔡徐坤" 确实覆盖了旧键值对 {key:111,"小马"} 的 "小马"
8.完结
🆗,以上就是 TreeSet 和 TreeMap 的源码解读了,其实就是把 TreeMap 讲了两遍