Java集合之TreeMap源码解析

时间:2021-08-21 17:17:03

上期回顾

上期我从树型结构谈到了红黑树的概念以及自平衡的各种变化指路上期←戳,本期我将会对TreeMap结合红黑树理论进行解读。

首先,我们先来回忆一下红黑树的5条基本规则。

1.结点是红色或者黑色,

2.根结点为黑色,

3.每个叶子结点都是黑色的空结点,

4.每个红色结点的两个子结点都是黑色,

5.从任何一个结点到叶子结点的所有路径的黑色结点的个数相同。

 

TreeMap的基本结构

我们先来看一下TreeMap的成员属性,

1     private final Comparator<? super K> comparator;
2     private transient Entry<K,V> root;
3     private transient int size = 0;
4     private transient int modCount = 0;

第1行,比较器,主要是用来维持树结构的有序,可以为空,如果为空,那按照key的自然规则进行排序,

第2行,Entry的引用root,是树型结构的根,

第3行,记录map中数据元素的个数,

第4行,记录map中数据更新的次数,

 

Entry是TreeMap的一个静态内部类,一个Entry对应一个树型结构中的结点,下面我们来看一下Entry的成员属性和构造函数,

 1  static final class Entry<K,V> implements Map.Entry<K,V> {
 2         K key;
 3         V value;
 4         Entry<K,V> left;
 5         Entry<K,V> right;
 6         Entry<K,V> parent;
 7         boolean color = BLACK;
 8 
 9         Entry(K key, V value, Entry<K,V> parent) {
10             this.key = key;
11             this.value = value;
12             this.parent = parent;
13         }

第2行,键

第3行,值

第4行,左结点的引用

第5行,右结点的引用

第6行,父结点的引用

第7行,当前结点默认为黑色结点

第9行,构造函数。初始化键、值、父结点引用

 

红黑树中的第1条规则是结点是红色或者黑色,在TreeMap中声明了两个常量表示

1 private static final boolean RED   = false;
2 private static final boolean BLACK = true;

false表示红色,true表示黑色,根结点是黑色,因此是true。

 

TreeMap的put方法解读

下面我们解读一下map在新增元素时发生哪些动作?我们以一个public修饰的静态方法,返回值为void,参数为String数组的main方法

作为程序的切入口,如下一段代码,

1 public static void main(String[] args) {
2         TreeMap<Integer, Integer> map = new TreeMap<Integer, Integer>();
3         map.put(50,3001);
4         map.put(30,3002);
5         map.put(70,3003);
6 }

第2行,new TreeMap<Integer, Integer>(),是一个无参的构造函数,初始化比较器为null,

第3行,新增一个键为50,值为3001的元素,

我们来跟一下put方法,源代码如下,

 

 1 public V put(K key, V value) {
 2         Entry<K,V> t = root;
 3         if (t == null) {
 4             compare(key, key); // type (and possibly null) check
 5 
 6             root = new Entry<>(key, value, null);
 7             size = 1;
 8             modCount++;
 9             return null;
10         }
11         int cmp;
12         Entry<K,V> parent;
13         // split comparator and comparable paths
14         Comparator<? super K> cpr = comparator;
15         if (cpr != null) {
16             do {
17                 parent = t;
18                 cmp = cpr.compare(key, t.key);
19                 if (cmp < 0)
20                     t = t.left;
21                 else if (cmp > 0)
22                     t = t.right;
23                 else
24                     return t.setValue(value);
25             } while (t != null);
26         }
27         else {
28             if (key == null)
29                 throw new NullPointerException();
30             @SuppressWarnings("unchecked")
31                 Comparable<? super K> k = (Comparable<? super K>) key;
32             do {
33                 parent = t;
34                 cmp = k.compareTo(t.key);
35                 if (cmp < 0)
36                     t = t.left;
37                 else if (cmp > 0)
38                     t = t.right;
39                 else
40                     return t.setValue(value);
41             } while (t != null);
42         }
43         Entry<K,V> e = new Entry<>(key, value, parent);
44         if (cmp < 0)
45             parent.left = e;
46         else
47             parent.right = e;
48         fixAfterInsertion(e);
49         size++;
50         modCount++;
51         return null;
52 }

第2行,把变量t指向根结点的引用,

第3行,判断根结点是否为空,如果为空说明,当前数据元素时首次新增,

第4行,调用自定义的比较器或者自然比较器,自身与自身做比较,旨在判断key是否是null,如果为空,则抛出NPE,

因此,TreeMap中的Key不允许为空。

第6-9行,把root引用指向当前元素,并且树结构的数量size更新为1,modCount自增,结束,返回值为null,

 

第11行,声明一个int类型的cmp,用来记录比较器的返回值

第12行,声明一个Entry类型的parent,用来记录待存储元素的父结点。上面Entry的构造函数中,其中一个数据项就是父结点,

 

第15行-26行,当比较器不为空时,找出待存储元素的父结点,下面我们来逐行阅读

第18行,根据我们自定义的比较规则,待存储元素的键和当前结点比较大小,

第19行,如果小于当前结点的键,那么下一个比较的目标应在在左子树上,

第21行,如果大于当前结点的键,那么下一个比较的目标应该在右子树上,

第24行,如果待存储的键等于当前结点的键,那么将新值更新,同时返回旧值。

 

第27行-42行,当比较器为空时,采用默认的自然比较规则,找出父结点上述基本相似,

注意:第28行,对key是否为空校验,因此:TreeMap中的Key不允许为空。

 

第43行,赋值,

第44行-47行,确定新增元素时父结点的左结点还是右结点。

第49行-50行,size和modCount分别自增,

 

接下来我们着来分析第48行,fixAfterInsertion(e),跟一下代码,

 1 private void fixAfterInsertion(Entry<K,V> x) {
 2         x.color = RED;
 3 
 4         while (x != null && x != root && x.parent.color == RED) {
 5             if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
 6                 Entry<K,V> y = rightOf(parentOf(parentOf(x)));
 7                 if (colorOf(y) == RED) {
 8                     setColor(parentOf(x), BLACK);
 9                     setColor(y, BLACK);
10                     setColor(parentOf(parentOf(x)), RED);
11                     x = parentOf(parentOf(x));
12                 } else {
13                     if (x == rightOf(parentOf(x))) {
14                         x = parentOf(x);
15                         rotateLeft(x);
16                     }
17                     setColor(parentOf(x), BLACK);
18                     setColor(parentOf(parentOf(x)), RED);
19                     rotateRight(parentOf(parentOf(x)));
20                 }
21             } else {
22                 Entry<K,V> y = leftOf(parentOf(parentOf(x)));
23                 if (colorOf(y) == RED) {
24                     setColor(parentOf(x), BLACK);
25                     setColor(y, BLACK);
26                     setColor(parentOf(parentOf(x)), RED);
27                     x = parentOf(parentOf(x));
28                 } else {
29                     if (x == leftOf(parentOf(x))) {
30                         x = parentOf(x);
31                         rotateRight(x);
32                     }
33                     setColor(parentOf(x), BLACK);
34                     setColor(parentOf(parentOf(x)), RED);
35                     rotateLeft(parentOf(parentOf(x)));
36                 }
37             }
38         }
39         root.color = BLACK;
40 }

 夜,已深,至于fixAfterInsertion  how do it work ? 且听下回分解。