【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;
}

附:添加结点的规则

image

3-13.7 Map 集合小结

常见问题:

  • 问:使用 TreeMap 添加元素时,是否需要 Java Bean 类重写 equalshashCode 方法?

    答:不需要,TreeMapput 方法并没有调用 Java Bean 类的 equalshashCode 方法。

  • 问:自 JDK 8 后,HashMap 也采用了红黑树的结构,那么 HashMap 是否要求键的 Java Bean 类实现 Comparable 接口或传递 Comparator 对象呢?

    答:不需要,HashMap 中的红黑树通过比较键的哈希值来比较键的大小关系。

  • 问:TreeMapHashMap 谁的效率更高?

    答:在最坏情况下,哈希表中的所有元素都形成了一个链表或红黑树,查找效率低下,但这种概率极小。一般而言,认为 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

posted @ 2023-08-13 00:50  Zebt  阅读(22)  评论(0)    收藏  举报