TreeMap源码解析和设计思路

一、TreeMap有两种排序方式

  1. 自然排序(Natural Order)

自然排序是TreeMap默认的排序方式,它根据键对象的自然顺序对键进行排序。如果键对象实现了Comparable接口,TreeMap将使用对象的compareTo方法来比较键的大小。如果键对象没有实现Comparable接口,则会在构造时抛出NullPointerException异常。

// 自然排序,对键排序
TreeMap<String, Integer> naturalOrderMap = new TreeMap<>();
naturalOrderMap.put("apple", 1);
naturalOrderMap.put("orange", 3);
naturalOrderMap.put("banana", 2);


System.out.println(naturalOrderMap); // 输出: {apple=1, banana=2, orange=3}
  1. 定制排序(Custom Order)

如果自然排序无法满足需求,可以在构造TreeMap时传入一个Comparator对象,用于定制排序规则。Comparator接口定义了compare方法

// 定制排序 - Key按字符串长度排序
Comparator<String> lengthComparator = (s1, s2) -> Integer.compare(s1.length(), s2.length());
TreeMap<String, Integer> customOrderMap = new TreeMap<>(lengthComparator);
customOrderMap.put("pear", 4);
customOrderMap.put("apple", 1);
customOrderMap.put("banana", 2);
customOrderMap.put("orange", 3);

System.out.println(customOrderMap); // 输出: {pear=4, apple=1, orange=3, banana=2}

二、类结构源码和初始化对象

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
    /**
     * 用于维护树的排序规则的比较器。
     * 如果为null,则使用键对象的自然排序。
     */
    private final Comparator<? super K> comparator;

    // 红黑树的根节点
    private transient Entry<K,V> root;

    /**
     * 树中键值对的数量
     */
    private transient int size = 0;

    /**
     * 记录对树的结构修改次数,用于支持fail-fast机制
     */
    private transient int modCount = 0;

    /**
     * 构造一个空的TreeMap,使用键对象的自然排序。
     * 要求所有键都实现Comparable接口,并且互相可比较。
     */
    public TreeMap() {
        comparator = null;
    }

    /**
     * 构造一个空的TreeMap,使用指定的比较器排序。
     * 要求所有键都通过比较器可比较。
     *
     * @param comparator 用于排序的比较器,如果为null则使用键对象自然排序
     */
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

    /**
     * 根据给定的Map构造TreeMap,使用键对象自然排序。
     * 要求所有键都实现Comparable接口,并且互相可比较。
     * 时间复杂度为O(n*log(n))。
     *
     * @param m 提供初始映射关系的Map
     * @throws ClassCastException 如果键不可比较
     * @throws NullPointerException 如果给定Map为null
     */
    public TreeMap(Map<? extends K, ? extends V> m) {
        comparator = null;
        putAll(m);
    }

    /**
     * 根据给定的有序Map构造TreeMap,使用与之相同的排序规则。
     * 时间复杂度为O(n)。
     *
     * @param m 提供初始映射关系的有序Map
     * @throws NullPointerException 如果给定Map为null
     */
    public TreeMap(SortedMap<K, ? extends V> m) {
        comparator = m.comparator();
        try {
            buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
        } catch (java.io.IOException cannotHappen) {
        } catch (ClassNotFoundException cannotHappen) {
        }
    }
	
	//红黑树的节点
	static final class Entry<K,V> implements Map.Entry<K,V> {
	}
}

TreeMap继承自AbstractMap,实现了NavigableMap、Cloneable和Serializable接口。其中NavigableMap继承自SortedMap,允许键值对按升序或降序遍历。Cloneable支持克隆操作,Serializable支持序列化操作。

  • 无参构造器创建空树使用自然排序
  • 带比较器的构造器使用指定的排序规则
  • Map构造则使用键对象自然排序构建树
  • SortedMap构造可以利用其有序性

三、put添加源码

好的,让我们来分析一下这段put方法的源码:

public V put(K key, V value) {
    // 获取红黑树根节点
    Entry<K,V> t = root;

    // 1.判断红黑树的节点是否为空,为空的话,新增的节点直接作为根节点
    if (t == null) {
        // 检查键对象是否可比较
        compare(key, key);

        // 创建一个新的根节点Entry
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }

    // 声明一些变量
    int cmp;
    Entry<K,V> parent;
    // 获取比较器
    Comparator<? super K> cpr = comparator;

    // 2.如果使用自定义比较器
    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
                return t.setValue(value);
        } while (t != null);
    }
    // 3.如果使用自然排序
    else {
        // 如果键为null,抛出NullPointerException
        if (key == null)
            throw new NullPointerException();
        // 获取键的Comparable视图
        @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            // 使用键对象的compareTo方法比较
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }

    // 找到插入位置,创建新节点
    Entry<K,V> e = new Entry<>(key, value, parent);
    // 将新节点链接到父节点
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    // 调整树结构,维护平衡
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}
  1. 如果树为空,直接创建一个根节点并返回。
  2. 如果使用自定义比较器,则使用comparator.compare方法比较键;否则使用键对象自身的compareTo方法比较。
  3. 沿着树从根节点开始遍历,根据比较结果选择左子树还是右子树,直到找到插入位置或遇到重复键。
  4. 如果找到重复键,直接更新其值并返回旧值。
  5. 如果未找到重复键,创建新节点,并将其链接到父节点中合适的位置。
  6. 调用fixAfterInsertion方法调整树结构,维护平衡。
  7. 更新size大小和modCount计数器。

需要注意的几点:

  1. 如果使用自然排序,键对象必须实现Comparable接口,否则会抛出ClassCastException。
  2. 如果键为null且使用自然排序,会抛出NullPointerException。
  3. 在比较时使用了null-safe的compare方法,避免传入null导致NullPointerException。

四、复现为啥必须实现Comparable接口?

import java.util.TreeMap;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 没有实现Comparable接口
}

public class TreeMapDemo {
    public static void main(String[] args) {
        // 创建一个TreeMap,使用自然排序
        TreeMap<Person, String> treeMap = new TreeMap<>();

        // 添加一些键值对
        treeMap.put(new Person("Alice", 25), "Alice's value");
        treeMap.put(new Person("Bob", 30), "Bob's value");
        treeMap.put(new Person("Charlie", 35), "Charlie's value");

        // 这里会抛出ClassCastException
        // 因为Person类没有实现Comparable接口
        treeMap.put(new Person("David", 40), "David's value");
    }
}

定义了一个Person类,但是没有实现Comparable接口。然后,我们创建了一个TreeMap对象,并尝试使用Person对象作为键。

当我们执行treeMap.put(new Person("David", 40), "David's value");语句时,会抛出ClassCastException异常,因为TreeMap在使用自然排序时,要求键对象必须实现Comparable接口。

解决方案:

  1. Person类实现Comparable接口
class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(Person other) {
        // 按照age进行排序
        return Integer.compare(this.age, other.age);
    }
}

Person类实现了Comparable接口,并在compareTo方法中实现了按照age进行排序的逻辑。这样,当我们使用Person对象作为TreeMap的键时,就不会再抛出ClassCastException异常了。

五、get查询源码

根据给定的键对象获取对应的树Entry节点

  	public V get(Object key) {
        Entry<K, V> p = getEntry(key);
        return (p == null ? null : p.value);
    }

    final Entry<K, V> getEntry(Object key) {
        // 使用自定义比较器,则调用getEntryUsingComparator方法
        if (comparator != null)
            return getEntryUsingComparator(key);

        // 如果键为null且使用自然排序,抛出NullPointerException
        if (key == null)
            throw new NullPointerException();

        // 获取键对象的Comparable视图
        @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;

        // 从根节点开始遍历
        Entry<K, V> p = root;
        while (p != null) {
            // 使用键对象的compareTo方法比较
            int cmp = k.compareTo(p.key);
            // 根据比较结果选择左子树或右子树
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
                // 如果找到对应的节点,返回该节点
            else
                return p;
        }
        // 未找到对应节点,返回null
        return null;
    }
  1. 首先检查是否使用了自定义比较器。如果使用了,则调用getEntryUsingComparator方法,该方法的实现类似,只是使用comparator.compare方法进行比较。

  2. 如果使用自然排序,并且传入的键为null,则抛出NullPointerException。

  3. 获取键对象的Comparable视图,用于后续比较。

  4. 从根节点开始遍历树。使用键对象的compareTo方法与当前节点的键进行比较,根据比较结果选择左子树还是右子树继续遍历。

  5. 如果找到与键对象相等的节点,直接返回该节点。

  6. 如果遍历完整棵树还未找到对应节点,返回null。

六、总结

  1. 数据结构

    • TreeMap底层使用红黑树(Red-Black Tree),和 HashMap 的红黑树结构一样。红黑树是一种自平衡的二叉查找树。
    • 每个节点(Entry)存储键值对数据,并通过左右子节点指针维护树的结构。
  2. 排序规则

    • TreeMap支持两种排序方式:自然排序(Natural Order)和自定义排序(Custom Order)。
    • 自然排序要求键对象实现Comparable接口,并根据对象的compareTo方法进行比较。
    • 自定义排序通过在构造时传入Comparator对象来指定排序规则。
  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java语录精选

你的鼓励是我坚持下去的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值