【Java集合框架】3 - 13 TreeMap 源码分析
§3-13 TreeMap
源码分析
3-13.1 TreeMap
中定义的字段与类
TreeMap
中定义了以下内部类,用于记录每一个数据结点:
Entry
:实现自 Map.Entry
接口,记录条目
static final class Entry<K,V> implements Map.Entry<K,V> {
K key; //键
V value; //值
Entry<K,V> left; //左子结点
Entry<K,V> right; //右子结点
Entry<K,V> parent; //父结点
boolean color = BLACK; //颜色,这里使用枚举常量定义
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
...
}
TreeMap
中定义了两个颜色相关的枚举常量:
private static final boolean RED = false;
private static final boolean BLACK = true;
在添加结点时,方法会进一步调整结点的颜色。
TreeMap
中定义了以下字段:
private final Comparator<? super K> comparator; //比较器对象
private transient Entry<K,V> root; //根结点
private transient int size = 0; //元素个数
3-13.2 构造器与 put
添加数据元素
首先来看无参构造:
public TreeMap() {
comparator = null; //无比较器对象
}
再来看看带比较器对象的构造器:
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
和 HashMap
相同,构造器并未对成员做更多的操作。这里,构造器仅仅负责比较器的赋值。
创建完成后,往表中添加第一个元素,put
方法的实现为:
public V put(K key, V value) {
return put(key, value, true);
}
put
方法在底层调用了另外一个带有三个参数的 put
重载,该重载的声明为:
private V put(K key, V value, boolean replaceOld)
可见,第三个参数用于控制重复元素的覆盖行为。true
表示用新值覆盖旧值,并返回旧值。
3-13.3 put
重载底层实现
请记住,TreeMap
在底层使用了红黑树这种数据结构。在阅读源码时,应当时刻将这种数据结构记于心中,并能够通过源码呼应数据结构中的有关运算。
put
的重载实现了数据元素的添加,其实现(内容较长)如下:
private V put(K key, V value, boolean replaceOld) {
Entry<K,V> t = root;
if (t == null) {
addEntryToEmptyMap(key, value);
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else {
V oldValue = t.value;
if (replaceOld || oldValue == null) {
t.value = value;
}
return oldValue;
}
} while (t != null);
} else {
Objects.requireNonNull(key);
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else {
V oldValue = t.value;
if (replaceOld || oldValue == null) {
t.value = value;
}
return oldValue;
}
} while (t != null);
}
addEntry(key, value, parent, cmp < 0);
return null;
}
接下来将逐句分析 put
重载的执行过程。
3-13.4 put
重载的完整注释
此处将实现细节以单行注释的形式逐句表示在完整方法中,提高可读性。
//外部方法 put 在底层调用了它的重载:put(key, value, true);
private V put(K key, V value, boolean replaceOld) {
//获取根结点地址
Entry<K,V> t = root;
//判断根结点是否为空
if (t == null) {
//是,则意味着当前映射表为空
//向该空映射表中添加根结点,该方法解释见下文
addEntryToEmptyMap(key, value);
//无元素需要覆盖,返回 null
return null;
}
//若已存在根结点
int cmp; //表示两个元素的键比较后的结果
Entry<K,V> parent; //父结点
//比较规则
Comparator<? super K> cpr = comparator; //指向当前映射表的比较器
//若已指定比较器,以比较器所指定的比较规则为准
if (cpr != null) {
do {
//首次执行时,将根结点视为当前结点的父结点
parent = t;
cmp = cpr.compare(key, t.key); //调用比较器的 compare 方法,计算比较结果
if (cmp < 0) //负数,表示待添加的结点小于父结点
t = t.left; //将当前结点设置为左子结点
else if (cmp > 0) //正数,表示待添加的结点大于父结点
t = t.right; //将当前结点设置为右子结点
else { //为零,表示二者相等
V oldValue = t.value; //记录重复项的旧值
if (replaceOld || oldValue == null) { //确定重复项的覆盖行为,此处为 true,进行覆盖
t.value = value; //用新值覆盖旧值
}
return oldValue; //返回旧值
}
} while (t != null); //寻找添加位置(非空条件)
} else {
//若未指定比较器,即使用空参构造,则使用可比接口的比较规则
Objects.requireNonNull(key); //要求键非空
@SuppressWarnings("unchecked")
//强制转换键为 Comparable 接口实现类对象,因此应当实现该接口并重写 compareTo 方法
Comparable<? super K> k = (Comparable<? super K>) key;
do {
//首次执行时,将根结点视为当前结点的父结点
parent = t;
cmp = k.compareTo(t.key); //调用可比接口的 compareTo 方法,计算比较结果
if (cmp < 0) //负数,表示待添加的结点小于父结点
t = t.left; //将当前结点设置为左子结点
else if (cmp > 0) //正数,表示待添加的结点大于父结点
t = t.right; //将当前结点设置为右子结点
else { //为零,表示二者相等
V oldValue = t.value; //记录重复项的旧值
if (replaceOld || oldValue == null) { //确定重复项的覆盖行为,此处为 true,进行覆盖
t.value = value; //用新值覆盖旧值
}
return oldValue; //返回旧值
}
} while (t != null); //寻找添加位置(非空条件)
}
//非重复且遍历到树的叶结点时跳出循环和选择分支,并能够确定添加位置
addEntry(key, value, parent, cmp < 0); //在指定位置添加结点
return null; //添加完成,无需返回
}
3-13.5 addEntryToEmptyMap
方法实现
向空表添加元素时,会调用该方法,其底层实现为:
private void addEntryToEmptyMap(K key, V value) {
//自己与自己比较,意义不大
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null); //添加根结点
size = 1; //修改元素个数
modCount++; //修改结构性修改次数
}
3-13.6 addEntry
添加新条目与红黑树修复
向表中添加新的元素,调用的是该方法,其实现为:
//put 方法结束前调用该方法:addEntry(key, value, parent, cmp < 0);
private void addEntry(K key, V value, Entry<K, V> parent, boolean addToLeft) {
//新建条目,记录待添加的结点
Entry<K,V> e = new Entry<>(key, value, parent);
//判断添加位置并添加
if (addToLeft)
parent.left = e;
else
parent.right = e;
//修复红黑树,使其满足红黑规则
fixAfterInsertion(e);
size++; //修改元素个数
modCount++; //修改结构性修改次数
}
其中,方法调用了 fixAfterInsertion
方法修复红黑树,其实现为:
//由 addEntry 方法调用:fixAfterInsertion(e);
//传递的是所添加的结点,以所新添加的结点为当前节点
private void fixAfterInsertion(Entry<K,V> x) {
//修改当前结点的颜色为红色(此前默认为黑色)
x.color = RED;
//按照红黑规则修复红黑树
//循环条件为:当前结点不为空、不为根、父结点为红
//因为父结点为黑色、当前结点为根结点时,不需要做更多操作,只需要将根结点设置为黑色即可
while (x != null && x != root && x.parent.color == RED) {
//判断当前结点的父结点属于祖父结点的左 / 右子结点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//若父结点为祖父结点的左子结点
Entry<K,V> y = rightOf(parentOf(parentOf(x))); //记录叔结点(祖父的右子结点)
if (colorOf(y) == RED) { //父结点、叔结点为红
setColor(parentOf(x), BLACK); //将父结点设为黑色
setColor(y, BLACK); //将叔结点设为黑色
setColor(parentOf(parentOf(x)), RED); //将祖父结点设为红色
x = parentOf(parentOf(x)); //以祖父结点为当前结点,进入下一轮循环,进一步判断
} else { //父结点为红,叔结点为黑
//当前结点为父的右子结点,父结点为祖父的左子结点(右左)
if (x == rightOf(parentOf(x))) {
x = parentOf(x); //以父结点为当前节点
rotateLeft(x); //左旋当前结点,再进行判断(转换为左左)
}
//当前结点为父的左子结点,父结点为祖父的左子结点(左左)
setColor(parentOf(x), BLACK); //将父结点设为黑色
setColor(parentOf(parentOf(x)), RED); //设祖父结点设为红色
rotateRight(parentOf(parentOf(x))); //以祖父结点为支点,右旋
}
} else {
//若父结点为祖父结点的右子结点
Entry<K,V> y = leftOf(parentOf(parentOf(x))); //记录叔结点(祖父的左子结点)
if (colorOf(y) == RED) { //父结点、叔结点为红
setColor(parentOf(x), BLACK); //将父结点设为黑色
setColor(y, BLACK); //将叔结点设为黑色
setColor(parentOf(parentOf(x)), RED); //将祖父结点设为红色
x = parentOf(parentOf(x)); //以祖父结点为当前结点,进入下一轮循环,进一步判断
} else { //父结点为红,叔结点为黑
//当前结点为父的左子结点,父结点为祖父的右子结点(左右)
if (x == leftOf(parentOf(x))) {
x = parentOf(x); //将父结点设为当前结点
rotateRight(x); //右旋当前结点,再进行判断(转换为右右)
}
//当前结点为父的右子结点,父结点为祖父的右子结点(右右)
setColor(parentOf(x), BLACK); //将父结点设为黑色
setColor(parentOf(parentOf(x)), RED); //将祖父结点设为红色
rotateLeft(parentOf(parentOf(x))); //以祖父结点为支点,左旋
}
}
}
//循环结束,修改根结点颜色为黑色,使其满足红黑规则
root.color = BLACK;
}
附:添加结点的规则
3-13.7 Map
集合小结
常见问题:
-
问:使用
TreeMap
添加元素时,是否需要 Java Bean 类重写equals
和hashCode
方法?答:不需要,
TreeMap
的put
方法并没有调用 Java Bean 类的equals
和hashCode
方法。 -
问:自 JDK 8 后,
HashMap
也采用了红黑树的结构,那么HashMap
是否要求键的 Java Bean 类实现Comparable
接口或传递Comparator
对象呢?答:不需要,
HashMap
中的红黑树通过比较键的哈希值来比较键的大小关系。 -
问:
TreeMap
和HashMap
谁的效率更高?答:在最坏情况下,哈希表中的所有元素都形成了一个链表或红黑树,查找效率低下,但这种概率极小。一般而言,认为
HashMap
效率更高(元素只需一次查找即可)。 -
问:
Map
集合中,既然提供了键重复则覆盖值的添加方法,是否也提供了非覆盖的方法?答:也提供了非覆盖的方法。
在
HashMap
中,存在方法putIfAbsent
,不覆盖现有值:public V putIfAbsent(K key, V value) { //实际上就是通过控制形参 onlyIfAbsent 的值,从而控制覆盖行为 return putVal(hash(key), key, value, true, true); }
在
TreeMap
中,也存在方法putIfAbsent
,不覆盖现有值:public V putIfAbsent(K key, V value) { //实际上也是通过控制形参 replaceOld 的值,从而控制覆盖行为 return put(key, value, false); }
这是一种思想:代码中逻辑具有两面性,若知道了其中一面,且代码中发现某个变量可以控制其两面的行为,那么必有该逻辑的反面。
boolean
类型的控制变量,一般只有两面,因为boolean
只有两个值;而int
类型的控制变量,一般具有三面,因为int
可取多个值。
三种双列集合的选择:
默认使用 HashMap
,效率最高;若保证存取有序,则使用 LinkedHashMap
,若要进行排序,则使用 TreeMap
。