深度剖析 Java TreeMap:从源码洞悉其使用原理

189 阅读39分钟

深度剖析 Java TreeMap:从源码洞悉其使用原理

一、引言

在 Java 的集合框架中,TreeMap 是一个极具特色的映射(Map)实现。它不仅能像普通的 Map 一样存储键值对,还具备强大的排序功能,能够根据键的自然顺序或者指定的比较器对键进行排序。这使得 TreeMap 在需要有序存储和访问键值对的场景中表现出色,例如实现排行榜、区间查询等功能。

本文将从源码层面深入剖析 TreeMap 的内部结构、核心操作原理、性能特点以及使用场景等方面,带领读者全面了解这个强大的集合类。通过对 TreeMap 源码的详细解读,读者可以更好地理解其工作机制,从而在实际开发中更加高效地使用它。

二、TreeMap 概述

2.1 什么是 TreeMap

TreeMap 是 Java 集合框架中的一个类,它实现了 NavigableMap 接口,继承自 AbstractMap 类。TreeMap 基于红黑树(Red-Black Tree)数据结构实现,这是一种自平衡的二叉搜索树,它能够保证在插入、删除和查找操作时的时间复杂度为 O(log n),其中 n 是树中元素的数量。

与其他 Map 实现(如 HashMap)不同的是,TreeMap 会根据键的顺序对元素进行排序。默认情况下,它会使用键的自然顺序进行排序,如果键实现了 Comparable 接口;也可以通过传入一个自定义的 Comparator 来指定排序规则。

2.2 特点与优势

  • 有序性TreeMap 会根据键的顺序对元素进行排序,这使得它在需要有序遍历键值对的场景中非常有用。例如,在实现排行榜时,可以根据分数对用户进行排序。
  • 高效的查找和插入:由于采用了红黑树数据结构,TreeMap 在插入、删除和查找操作时的时间复杂度为 O(log n),保证了操作的高效性。
  • 支持范围查询TreeMap 提供了丰富的方法来进行范围查询,如 subMapheadMaptailMap 等,可以方便地获取指定范围内的键值对。

2.3 基本使用示例

import java.util.TreeMap;

public class TreeMapExample {
    public static void main(String[] args) {
        // 创建一个 TreeMap 实例,使用键的自然顺序进行排序
        TreeMap<Integer, String> treeMap = new TreeMap<>();

        // 向 TreeMap 中添加键值对
        treeMap.put(3, "Apple");
        treeMap.put(1, "Banana");
        treeMap.put(2, "Cherry");

        // 遍历 TreeMap,元素将按键的升序输出
        for (Integer key : treeMap.keySet()) {
            System.out.println("Key: " + key + ", Value: " + treeMap.get(key));
        }

        // 获取第一个键值对
        System.out.println("First entry: " + treeMap.firstEntry());

        // 获取最后一个键值对
        System.out.println("Last entry: " + treeMap.lastEntry());
    }
}

在上述示例中,我们创建了一个 TreeMap 实例,并向其中添加了一些键值对。然后,我们遍历 TreeMap,可以看到元素是按照键的升序输出的。最后,我们使用 firstEntrylastEntry 方法分别获取了第一个和最后一个键值对。

三、TreeMap 源码结构分析

3.1 类的定义与继承关系

// TreeMap 类继承自 AbstractMap 类,并实现了 NavigableMap 接口
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;

    // 记录结构修改次数,用于快速失败机制
    private transient int modCount = 0;

    // 构造函数,使用键的自然顺序进行排序
    public TreeMap() {
        comparator = null;
    }

    // 构造函数,使用指定的比较器进行排序
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

    // 其他构造函数和方法的定义...
}

从上述源码可以看出,TreeMap 继承自 AbstractMap 类,并实现了 NavigableMapCloneableSerializable 接口。它包含了几个重要的成员变量:comparator 用于指定键的排序规则,如果为 null,则使用键的自然顺序;root 是红黑树的根节点;size 表示树中元素的数量;modCount 用于记录结构修改次数,用于快速失败机制。

3.2 节点类的定义

// 静态内部类 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;
    // 节点的颜色,true 表示红色,false 表示黑色
    boolean color = BLACK;

    // 构造函数,初始化键、值和父节点
    Entry(K key, V value, Entry<K,V> parent) {
        this.key = key;
        this.value = value;
        this.parent = parent;
    }

    // 获取键
    public K getKey() {
        return key;
    }

    // 获取值
    public V getValue() {
        return value;
    }

    // 设置新的值,并返回旧的值
    public V setValue(V value) {
        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }

    // 判断两个 Entry 是否相等
    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;

        return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
    }

    // 计算 Entry 的哈希码
    public int hashCode() {
        int keyHash = (key==null? 0 : key.hashCode());
        int valueHash = (value==null? 0 : value.hashCode());
        return keyHash ^ valueHash;
    }

    // 将 Entry 转换为字符串表示
    public String toString() {
        return key + "=" + value;
    }
}

Entry 类是 TreeMap 中用于表示红黑树节点的静态内部类。每个节点包含键、值、左子节点、右子节点、父节点和颜色信息。它实现了 Map.Entry 接口,提供了获取键、值、设置值、判断相等性和计算哈希码等方法。

3.3 重要成员变量和方法概述

  • comparator:用于指定键的排序规则的比较器。如果为 null,则使用键的自然顺序。
  • root:红黑树的根节点。
  • size:树中元素的数量。
  • modCount:记录结构修改次数,用于快速失败机制。
  • put(K key, V value):向 TreeMap 中插入一个键值对。
  • get(Object key):根据键获取对应的值。
  • remove(Object key):根据键移除对应的键值对。
  • firstEntry():获取第一个键值对。
  • lastEntry():获取最后一个键值对。
  • subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive):获取指定范围内的子 TreeMap

下面我们将详细分析这些方法的实现原理。

四、TreeMap 核心操作原理分析

4.1 插入元素操作

4.1.1 put 方法
// 向 TreeMap 中插入一个键值对
public V put(K key, V value) {
    // 获取根节点
    Entry<K,V> t = root;
    // 如果根节点为空,说明树为空,创建一个新的节点作为根节点
    if (t == null) {
        // 检查键是否为 null,如果为 null 且比较器为 null,则抛出 NullPointerException
        compare(key, key); // type (and possibly null) check

        // 创建一个新的节点作为根节点
        root = new Entry<>(key, value, null);
        // 树中元素数量加 1
        size = 1;
        // 结构修改次数加 1
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    // 获取比较器
    Comparator<? super K> cpr = comparator;
    // 如果比较器不为 null
    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);
    }
    // 如果比较器为 null,使用键的自然顺序进行比较
    else {
        // 检查键是否为 null,如果为 null,则抛出 NullPointerException
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            // 将键转换为 Comparable 类型
            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
                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);
    // 树中元素数量加 1
    size++;
    // 结构修改次数加 1
    modCount++;
    return null;
}

// 比较两个键的大小
final int compare(Object k1, Object k2) {
    // 获取比较器
    return comparator==null? ((Comparable<? super K>)k1).compareTo((K)k2)
        : comparator.compare((K)k1, (K)k2);
}

put 方法用于向 TreeMap 中插入一个键值对。首先,它会检查根节点是否为空,如果为空,则创建一个新的节点作为根节点。然后,根据比较器(如果有)或键的自然顺序,从根节点开始遍历树,找到合适的插入位置。如果找到相同的键,则更新该节点的值并返回旧的值;否则,创建一个新的节点并插入到树中。最后,调用 fixAfterInsertion 方法进行红黑树的平衡调整。

4.1.2 fixAfterInsertion 方法
// 插入新节点后,进行红黑树的平衡调整
private void fixAfterInsertion(Entry<K,V> x) {
    // 新插入的节点颜色设为红色
    x.color = RED;

    // 当节点不为 null 且不是根节点,且父节点颜色为红色时
    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;
}

// 获取节点的父节点
private static <K,V> Entry<K,V> parentOf(Entry<K,V> p) {
    return (p == null? null: p.parent);
}

// 获取节点的左子节点
private static <K,V> Entry<K,V> leftOf(Entry<K,V> p) {
    return (p == null)? null: p.left;
}

// 获取节点的右子节点
private static <K,V> Entry<K,V> rightOf(Entry<K,V> p) {
    return (p == null)? null: p.right;
}

// 获取节点的颜色
private static <K,V> boolean colorOf(Entry<K,V> p) {
    return (p == null? BLACK : p.color);
}

// 设置节点的颜色
private static <K,V> void setColor(Entry<K,V> p, boolean c) {
    if (p != null)
        p.color = c;
}

// 左旋操作
private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        // 获取 p 的右子节点
        Entry<K,V> r = p.right;
        // 将 p 的右子节点更新为 r 的左子节点
        p.right = r.left;
        // 如果 r 的左子节点不为 null,将其父节点更新为 p
        if (r.left != null)
            r.left.parent = p;
        // 将 r 的父节点更新为 p 的父节点
        r.parent = p.parent;
        // 如果 p 的父节点为空,说明 p 是根节点,将 r 设为根节点
        if (p.parent == null)
            root = r;
        // 如果 p 是其父节点的左子节点,将 r 设为其父节点的左子节点
        else if (p.parent.left == p)
            p.parent.left = r;
        // 否则,将 r 设为其父节点的右子节点
        else
            p.parent.right = r;
        // 将 r 的左子节点设为 p
        r.left = p;
        // 将 p 的父节点设为 r
        p.parent = r;
    }
}

// 右旋操作
private void rotateRight(Entry<K,V> p) {
    if (p != null) {
        // 获取 p 的左子节点
        Entry<K,V> l = p.left;
        // 将 p 的左子节点更新为 l 的右子节点
        p.left = l.right;
        // 如果 l 的右子节点不为 null,将其父节点更新为 p
        if (l.right != null)
            l.right.parent = p;
        // 将 l 的父节点更新为 p 的父节点
        l.parent = p.parent;
        // 如果 p 的父节点为空,说明 p 是根节点,将 l 设为根节点
        if (p.parent == null)
            root = l;
        // 如果 p 是其父节点的右子节点,将 l 设为其父节点的右子节点
        else if (p.parent.right == p)
            p.parent.right = l;
        // 否则,将 l 设为其父节点的左子节点
        else
            p.parent.left = l;
        // 将 l 的右子节点设为 p
        l.right = p;
        // 将 p 的父节点设为 l
        p.parent = l;
    }
}

fixAfterInsertion 方法用于在插入新节点后进行红黑树的平衡调整,以保证红黑树的性质。红黑树有以下几个重要性质:

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点是黑色。
  3. 每个叶子节点(NIL 节点,空节点)是黑色。
  4. 如果一个节点是红色的,则它的子节点必须是黑色的。
  5. 对每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。

在插入新节点时,新节点的颜色默认为红色。如果插入后破坏了红黑树的性质,就需要进行调整。调整的主要操作包括变色和旋转(左旋和右旋)。具体来说,当父节点为红色时,根据叔叔节点的颜色和当前节点与父节点的位置关系,进行不同的处理:

  • 如果叔叔节点为红色,将父节点和叔叔节点颜色设为黑色,祖父节点颜色设为红色,然后将当前节点更新为祖父节点,继续向上调整。
  • 如果叔叔节点为黑色,根据当前节点与父节点的位置关系,进行左旋或右旋操作,然后进行相应的变色操作。

4.2 查找元素操作

4.2.1 get 方法
// 根据键获取对应的值
public V get(Object key) {
    // 调用 getEntry 方法查找节点
    Entry<K,V> p = getEntry(key);
    // 如果找到节点,返回其值;否则返回 null
    return (p==null? null : p.value);
}

// 根据键查找节点
final Entry<K,V> getEntry(Object key) {
    // 如果比较器不为 null,调用 getEntryUsingComparator 方法查找节点
    if (comparator != null)
        return getEntryUsingComparator(key);
    // 如果键为 null,抛出 NullPointerException
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
        // 将键转换为 Comparable 类型
        Comparable<? super K> k = (Comparable<? super K>) key;
    // 从根节点开始遍历树
    Entry<K,V> p = root;
    while (p != null) {
        // 比较键的大小
        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;
}

// 使用比较器查找节点
final Entry<K,V> getEntryUsingComparator(Object key) {
    @SuppressWarnings("unchecked")
        // 将键转换为 K 类型
        K k = (K) key;
    // 获取比较器
    Comparator<? super K> cpr = comparator;
    // 从根节点开始遍历树
    if (cpr != null) {
        Entry<K,V> p = root;
        while (p != null) {
            // 比较键的大小
            int cmp = cpr.compare(k, p.key);
            // 如果键小于当前节点的键,向左子树遍历
            if (cmp < 0)
                p = p.left;
            // 如果键大于当前节点的键,向右子树遍历
            else if (cmp > 0)
                p = p.right;
            // 如果键等于当前节点的键,返回该节点
            else
                return p;
        }
    }
    // 未找到节点,返回 null
    return null;
}

get 方法用于根据键获取对应的值。它首先调用 getEntry 方法查找节点,如果找到节点,则返回其值;否则返回 nullgetEntry 方法根据比较器的情况,选择使用 getEntryUsingComparator 方法或键的自然顺序进行查找。查找过程从根节点开始,根据键的大小关系,向左或向右子树遍历,直到找到相同的键或遍历到叶子节点。

4.3 删除元素操作

4.3.1 remove 方法
// 根据键移除对应的键值对
public V remove(Object key) {
    // 调用 getEntry 方法查找节点
    Entry<K,V> p = getEntry(key);
    // 如果未找到节点,返回 null
    if (p == null)
        return null;

    // 保存节点的值
    V oldValue = p.value;
    // 调用 deleteEntry 方法删除节点
    deleteEntry(p);
    // 返回旧的值
    return oldValue;
}

// 删除节点
private void deleteEntry(Entry<K,V> p) {
    // 结构修改次数加 1
    modCount++;
    // 树中元素数量减 1
    size--;

    // If strictly internal, copy successor's element to p and then make p
    // point to successor.
    // 如果节点有两个子节点
    if (p.left != null && p.right != null) {
        // 找到后继节点
        Entry<K,V> s = successor(p);
        // 将后继节点的键和值复制到当前节点
        p.key = s.key;
        p.value = s.value;
        // 将当前节点更新为后继节点
        p = s;
    } // p has 2 children

    // Start fixup at replacement node, if it exists.
    // 获取替换节点
    Entry<K,V> replacement = (p.left != null? p.left : p.right);

    // 如果替换节点不为 null
    if (replacement != null) {
        // Link replacement to parent
        // 将替换节点的父节点更新为当前节点的父节点
        replacement.parent = p.parent;
        // 如果当前节点的父节点为空,说明当前节点是根节点,将替换节点设为根节点
        if (p.parent == null)
            root = replacement;
        // 如果当前节点是其父节点的左子节点,将替换节点设为其父节点的左子节点
        else if (p == p.parent.left)
            p.parent.left  = replacement;
        // 否则,将替换节点设为其父节点的右子节点
        else
            p.parent.right = replacement;

        // Null out links so they are OK to use by fixAfterDeletion.
        // 将当前节点的左右子节点和父节点置为 null
        p.left = p.right = p.parent = null;

        // Fix replacement
        // 如果当前节点颜色为黑色,进行红黑树的平衡调整
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) { // return if we are the only node.
        // 如果当前节点是根节点且没有子节点,将根节点置为 null
        root = null;
    } else { //  No children. Use self as phantom replacement and unlink.
        // 如果当前节点颜色为黑色,进行红黑树的平衡调整
        if (p.color == BLACK)
            fixAfterDeletion(p);

        // 如果当前节点有父节点,将其父节点对应的子节点置为 null
        if (p.parent != null) {
            if (p == p.parent.left)
                p.parent.left = null;
            else if (p == p.parent.right)
                p.parent.right = null;
            // 将当前节点的父节点置为 null
            p.parent = null;
        }
    }
}

// 找到节点的后继节点
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
    // 如果节点为空,返回 null
    if (t == null)
        return null;
    // 如果节点有右子节点
    else if (t.right != null) {
        // 从右子节点开始,一直向左子树遍历,找到最小的节点
        Entry<K,V> p = t.right;
        while (p.left != null)
            p = p.left;
        return p;
    } else {
        // 如果节点没有右子节点,向上查找,直到找到一个节点是其父节点的左子节点
        Entry<K,V> p = t.parent;
        Entry<K,V> ch = t;
        while (p != null && ch == p.right) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}

remove 方法用于根据键移除对应的键值对。它首先调用 getEntry 方法查找节点,如果找到节点,则调用 deleteEntry 方法删除该节点。deleteEntry 方法处理三种情况:

  1. 如果节点有两个子节点,找到其后继节点,将后继节点的键和值复制到当前节点,然后将当前节点更新为后继节点。
  2. 如果节点有一个子节点,将子节点替换当前节点的位置。
  3. 如果节点没有子节点,直接移除该节点。

在删除节点后,如果删除的节点颜色为黑色,可能会破坏红黑树的性质,需要调用 fixAfterDeletion 方法进行平衡调整。

4.3.2 fixAfterDeletion 方法
// 删除节点后,进行红黑树的平衡调整
private void fixAfterDeletion(Entry<K,V> x) {
    // 当节点不为根节点且颜色为黑色时
    while (x != root && colorOf(x) == BLACK) {
        // 如果节点是其父节点的左子节点
        if (x == leftOf(parentOf(x))) {
            // 获取兄弟节点
            Entry<K,V> sib = rightOf(parentOf(x));

            // 如果兄弟节点颜色为红色
            if (colorOf(sib) == RED) {
                // 将兄弟节点颜色设为黑色
                setColor(sib, BLACK);
                // 将父节点颜色设为红色
                setColor(parentOf(x), RED);
                // 左旋操作
                rotateLeft(parentOf(x));
                // 更新兄弟节点
                sib = rightOf(parentOf(x));
            }

            // 如果兄弟节点的左子节点和右子节点颜色都为黑色
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                // 将兄弟节点颜色设为红色
                setColor(sib, RED);
                // 将当前节点更新为父节点
                x = parentOf(x);
            } else {
                // 如果兄弟节点的右子节点颜色为黑色
                if (colorOf(rightOf(sib)) == BLACK) {
                    // 将兄弟节点的左子节点颜色设为黑色
                    setColor(leftOf(sib), BLACK);
                    // 将兄弟节点颜色设为红色
                    setColor(sib, RED);
                    // 右旋操作
                    rotateRight(sib);
                    // 更新兄弟节点
                    sib = rightOf(parentOf(x));
                }
                // 将兄弟节点颜色设为父节点的颜色
                setColor(sib, colorOf(parentOf(x)));
                // 将父节点颜色设为黑色
                setColor(parentOf(x), BLACK);
                // 将兄弟节点的右子节点颜色设为黑色
                setColor(rightOf(sib), BLACK);
                // 左旋操作
                rotateLeft(parentOf(x));
                // 将当前节点更新为根节点
                x = root;
            }
        } else { // symmetric
            // 如果节点是其父节点的右子节点
            Entry<K,V> sib = leftOf(parentOf(x));

            // 如果兄弟节点颜色为红色
            if (colorOf(sib) == RED) {
                // 将兄弟节点颜色设为黑色
                setColor(sib, BLACK);
                // 将父节点颜色设为红色
                setColor(parentOf(x), RED);
                // 右旋操作
                rotateRight(parentOf(x));
                // 更新兄弟节点
                sib = leftOf(parentOf(x));
            }

            // 如果兄弟节点的左子节点和右子节点颜色都为黑色
            if (colorOf(rightOf(sib)) == BLACK &&
                colorOf(leftOf(sib))  == BLACK) {
                // 将兄弟节点颜色设为红色
                setColor(sib, RED);
                // 将当前节点更新为父节点
                x = parentOf(x);
            } else {
                // 如果兄弟节点的左子节点颜色为黑色
                if (colorOf(leftOf(sib)) == BLACK) {
                    // 将兄弟节点的右子节点颜色设为黑色
                    setColor(rightOf(sib), BLACK);
                    // 将兄弟节点颜色设为红色
                    setColor(sib, RED);
                    // 左旋操作
                    rotateLeft(sib);
                    // 更新兄弟节点
                    sib = leftOf(parentOf(x));
                }
                // 将兄弟节点颜色设为父节点的颜色
                setColor(sib, colorOf(parentOf(x)));
                // 将父节点颜色设为黑色
                setColor(parentOf(x), BLACK);
                // 将兄弟节点的左子节点颜色设为黑色
                setColor(leftOf(sib), BLACK);
                // 右旋操作
                rotateRight(parentOf(x));
                // 将当前节点更新为根节点
                x = root;
            }
        }
    }

    // 将当前节点颜色设为黑色
    setColor(x, BLACK);
}

fixAfterDeletion 方法用于在删除节点后进行红黑树的平衡调整。当删除的节点颜色为黑色时,可能会破坏红黑树的性质,需要进行调整。调整的主要操作包括变色和旋转(左旋和右旋)。具体来说,根据当前节点和兄弟节点的颜色以及兄弟节点子节点的颜色,进行不同的处理:

  1. 如果兄弟节点颜色为红色,将兄弟节点颜色设为黑色,父节点颜色设为红色,进行左旋或右旋操作,然后更新兄弟节点。
  2. 如果兄弟节点的左右子节点颜色都为黑色,将兄弟节点颜色设为红色,将当前节点更新为父节点,继续向上调整。
  3. 如果兄弟节点的某个子节点颜色为红色,进行相应的变色和旋转操作,使红黑树恢复平衡。

4.4 遍历操作

4.4.1 迭代器的实现

TreeMap 提供了多种迭代器来遍历键值对,如 KeySet 迭代器、Values 迭代器和 EntrySet 迭代器。以下是 KeySet 迭代器的部分源码:

// KeySet 类,继承自 AbstractSet 类
final class KeySet extends AbstractSet<K> {
    // 获取迭代器
    public Iterator<K> iterator() {
        return new KeyIterator(getFirstEntry());
    }

    // 获取元素数量
    public int size() {
        return TreeMap.this.size();
    }

    // 判断是否包含指定元素
    public boolean contains(Object o) {
        return TreeMap.this.containsKey(o);
    }

    // 移除指定元素
    public boolean remove(Object o) {
        int oldSize = size();
        TreeMap.this.remove(o);
        return size() != oldSize;
    }

    // 清空集合
    public void clear() {
        TreeMap.this.clear();
    }
}

// 键迭代器类,继承自 PrivateEntryIterator 类
final class KeyIterator extends PrivateEntryIterator<K> {
    // 构造函数,初始化迭代器
    KeyIterator(Entry<K,V> first) {
        super(first);
    }

    // 获取下一个键
    public K next() {
        return nextEntry().key;
    }
}

// 私有条目迭代器类
abstract class PrivateEntryIterator<T> implements Iterator<T> {
    // 下一个要访问的节点
    Entry<K,V> next;
    // 最后访问的节点
    Entry<K,V> lastReturned;
    // 记录结构修改次数
    int expectedModCount;

    // 构造函数,初始化迭代器
    PrivateEntryIterator(Entry<K,V> first) {
        // 记录结构修改次数
        expectedModCount = modCount;
        // 最后访问的节点置为 null
        lastReturned = null;
        // 下一个要访问的节点设为第一个节点
        next = first;
    }

    // 判断是否还有下一个元素
    public final boolean hasNext() {
        return next != null;
    }

4.4 遍历操作(续)

4.4.1 迭代器的实现(续)
    // 获取下一个条目
    final Entry<K,V> nextEntry() {
        // 获取下一个要访问的节点
        Entry<K,V> e = next;
        // 如果下一个节点为空,抛出 NoSuchElementException 异常
        if (e == null)
            throw new NoSuchElementException();
        // 检查结构修改次数是否一致,如果不一致,抛出 ConcurrentModificationException 异常
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        // 更新下一个要访问的节点为后继节点
        next = successor(e);
        // 记录最后访问的节点
        lastReturned = e;
        // 返回当前节点
        return e;
    }

    // 获取上一个条目
    final Entry<K,V> prevEntry() {
        // 获取下一个要访问的节点
        Entry<K,V> e = next;
        // 如果下一个节点为空,抛出 NoSuchElementException 异常
        if (e == null)
            throw new NoSuchElementException();
        // 检查结构修改次数是否一致,如果不一致,抛出 ConcurrentModificationException 异常
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        // 更新下一个要访问的节点为前驱节点
        next = predecessor(e);
        // 记录最后访问的节点
        lastReturned = e;
        // 返回当前节点
        return e;
    }

    // 移除当前元素
    public void remove() {
        // 如果最后访问的节点为空,抛出 IllegalStateException 异常
        if (lastReturned == null)
            throw new IllegalStateException();
        // 检查结构修改次数是否一致,如果不一致,抛出 ConcurrentModificationException 异常
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        // 删除最后访问的节点
        if (lastReturned.left != null && lastReturned.right != null)
            next = lastReturned;
        // 调用 deleteEntry 方法删除节点
        deleteEntry(lastReturned);
        // 更新结构修改次数
        expectedModCount = modCount;
        // 最后访问的节点置为 null
        lastReturned = null;
    }

    // 找到节点的前驱节点
    static <K,V> TreeMap.Entry<K,V> predecessor(Entry<K,V> t) {
        // 如果节点为空,返回 null
        if (t == null)
            return null;
        // 如果节点有左子节点
        else if (t.left != null) {
            // 从左子节点开始,一直向右子树遍历,找到最大的节点
            Entry<K,V> p = t.left;
            while (p.right != null)
                p = p.right;
            return p;
        } else {
            // 如果节点没有左子节点,向上查找,直到找到一个节点是其父节点的右子节点
            Entry<K,V> p = t.parent;
            Entry<K,V> ch = t;
            while (p != null && ch == p.left) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }
}

TreeMap 的迭代器实现基于红黑树的结构。以 KeySet 迭代器为例,KeyIterator 继承自 PrivateEntryIterator。在 PrivateEntryIterator 中,next 变量记录下一个要访问的节点,lastReturned 记录最后访问的节点,expectedModCount 用于记录结构修改次数,以实现快速失败机制。

nextEntry 方法用于获取下一个节点,它会先检查下一个节点是否为空,然后检查结构修改次数是否一致,接着更新 next 为后继节点,并返回当前节点。prevEntry 方法类似,只是更新 next 为前驱节点。

remove 方法用于移除当前元素,它会先检查 lastReturned 是否为空,再检查结构修改次数,然后调用 deleteEntry 方法删除节点,并更新 expectedModCount

predecessor 方法用于找到节点的前驱节点。如果节点有左子节点,就从左子节点开始一直向右子树遍历,找到最大的节点;如果没有左子节点,就向上查找,直到找到一个节点是其父节点的右子节点。

4.4.2 不同遍历方式的性能分析
  • 顺序遍历:由于 TreeMap 是基于红黑树实现的,顺序遍历(如使用 KeySet 迭代器按键的升序遍历)的时间复杂度为 O(n),其中 n 是树中元素的数量。这是因为需要遍历树中的每个节点一次。在遍历过程中,通过不断找到后继节点来实现顺序访问。
  • 逆序遍历:逆序遍历可以通过找到前驱节点来实现,时间复杂度同样为 O(n)。因为也需要遍历树中的每个节点一次。

4.5 范围查询操作

4.5.1 subMap 方法
// 获取指定范围内的子 TreeMap
public NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive,
                                K toKey,   boolean toInclusive) {
    // 检查键的范围是否合法
    if (fromKey == null || toKey == null)
        throw new NullPointerException();
    // 检查比较器是否为 null
    if (comparator != null) {
        // 比较起始键和结束键的大小
        compare(fromKey, toKey);
    } else {
        // 如果比较器为 null,使用键的自然顺序比较
        @SuppressWarnings("unchecked")
        Comparable<? super K> ck = (Comparable<? super K>) fromKey;
        ck.compareTo(toKey);
    }
    // 创建一个 SubMap 实例
    return new SubMap<>(this,
                        false, fromKey, fromInclusive,
                        false, toKey,   toInclusive);
}

// SubMap 类,继承自 AbstractMap 类,并实现了 NavigableMap 接口
static final class SubMap<K,V> extends AbstractMap<K,V>
    implements NavigableMap<K,V>, java.io.Serializable
{
    // 父 TreeMap
    private final TreeMap<K,V> m;
    // 起始键是否包含
    private final boolean fromStart;
    // 起始键
    private final K fromKey;
    // 起始键是否包含
    private final boolean fromInclusive;
    // 是否到结束
    private final boolean toEnd;
    // 结束键
    private final K toKey;
    // 结束键是否包含
    private final boolean toInclusive;

    // 构造函数,初始化 SubMap
    SubMap(TreeMap<K,V> m,
           boolean fromStart, K fromKey, boolean fromInclusive,
           boolean toEnd,     K toKey,   boolean toInclusive) {
        // 检查参数是否合法
        if (!fromStart && !toEnd) {
            if (m.compare(fromKey, toKey) > 0)
                throw new IllegalArgumentException("fromKey > toKey");
        } else {
            if (!fromStart)
                m.compare(fromKey, fromKey);
            if (!toEnd)
                m.compare(toKey, toKey);
        }
        // 初始化成员变量
        this.m = m;
        this.fromStart = fromStart;
        this.fromKey = fromKey;
        this.fromInclusive = fromInclusive;
        this.toEnd = toEnd;
        this.toKey = toKey;
        this.toInclusive = toInclusive;
    }

    // 检查键是否在范围内
    private boolean inRange(K key) {
        // 如果从起始开始
        if (!fromStart) {
            // 比较键和起始键的大小
            int c = m.compare(key, fromKey);
            // 如果键小于起始键,或者键等于起始键但不包含起始键,返回 false
            if (c < 0 || (c == 0 && !fromInclusive))
                return false;
        }
        // 如果到结束
        if (!toEnd) {
            // 比较键和结束键的大小
            int c = m.compare(key, toKey);
            // 如果键大于结束键,或者键等于结束键但不包含结束键,返回 false
            if (c > 0 || (c == 0 && !toInclusive))
                return false;
        }
        // 键在范围内,返回 true
        return true;
    }

    // 其他方法的实现...
}

subMap 方法用于获取指定范围内的子 TreeMap。它首先检查键的范围是否合法,然后创建一个 SubMap 实例。SubMap 类继承自 AbstractMap 类,并实现了 NavigableMap 接口。

inRange 方法用于检查键是否在指定范围内。它会分别比较键和起始键、结束键的大小,并根据是否包含起始键和结束键来判断。

4.5.2 headMap 方法
// 获取小于指定键的子 TreeMap
public NavigableMap<K,V> headMap(K toKey, boolean inclusive) {
    // 检查键是否为 null
    if (toKey == null)
        throw new NullPointerException();
    // 创建一个 SubMap 实例
    return new SubMap<>(this,
                        true,  null,    false,
                        false, toKey,   inclusive);
}

headMap 方法用于获取小于指定键的子 TreeMap。它会创建一个 SubMap 实例,其中 fromStarttrue,表示从起始开始,fromKeynulltoKey 为指定的键,inclusive 表示是否包含结束键。

4.5.3 tailMap 方法
// 获取大于等于指定键的子 TreeMap
public NavigableMap<K,V> tailMap(K fromKey, boolean inclusive) {
    // 检查键是否为 null
    if (fromKey == null)
        throw new NullPointerException();
    // 创建一个 SubMap 实例
    return new SubMap<>(this,
                        false, fromKey, inclusive,
                        true,  null,    false);
}

tailMap 方法用于获取大于等于指定键的子 TreeMap。它会创建一个 SubMap 实例,其中 fromStartfalsefromKey 为指定的键,inclusive 表示是否包含起始键,toEndtrue,表示到结束,toKeynull

4.6 其他操作

4.6.1 firstEntry 方法
// 获取第一个键值对
public Map.Entry<K,V> firstEntry() {
    // 获取第一个节点
    return exportEntry(getFirstEntry());
}

// 获取第一个节点
final Entry<K,V> getFirstEntry() {
    // 从根节点开始,一直向左子树遍历,找到最小的节点
    Entry<K,V> p = root;
    if (p != null)
        while (p.left != null)
            p = p.left;
    return p;
}

// 导出节点为 Map.Entry
static <K,V> Map.Entry<K,V> exportEntry(TreeMap.Entry<K,V> e) {
    // 如果节点为空,返回 null
    return (e == null)? null :
        new AbstractMap.SimpleImmutableEntry<>(e);
}

firstEntry 方法用于获取第一个键值对。它会调用 getFirstEntry 方法找到第一个节点,然后调用 exportEntry 方法将节点导出为 Map.EntrygetFirstEntry 方法从根节点开始,一直向左子树遍历,找到最小的节点。

4.6.2 lastEntry 方法
// 获取最后一个键值对
public Map.Entry<K,V> lastEntry() {
    // 获取最后一个节点
    return exportEntry(getLastEntry());
}

// 获取最后一个节点
final Entry<K,V> getLastEntry() {
    // 从根节点开始,一直向右子树遍历,找到最大的节点
    Entry<K,V> p = root;
    if (p != null)
        while (p.right != null)
            p = p.right;
    return p;
}

lastEntry 方法用于获取最后一个键值对。它会调用 getLastEntry 方法找到最后一个节点,然后调用 exportEntry 方法将节点导出为 Map.EntrygetLastEntry 方法从根节点开始,一直向右子树遍历,找到最大的节点。

4.6.3 lowerEntry 方法
// 获取小于指定键的最大键值对
public Map.Entry<K,V> lowerEntry(K key) {
    // 从根节点开始查找
    Entry<K,V> p = lowerEntry(key, root);
    // 导出节点为 Map.Entry
    return exportEntry(p);
}

// 从指定节点开始查找小于指定键的最大节点
private Entry<K,V> lowerEntry(K key, Entry<K,V> p) {
    // 如果节点为空,返回 null
    if (p == null)
        return null;
    // 比较键和当前节点的键的大小
    int cmp = compare(key, p.key);
    // 如果键大于当前节点的键
    if (cmp > 0) {
        // 向右子树查找
        Entry<K,V> q = lowerEntry(key, p.right);
        // 如果右子树中找到的节点不为空,返回该节点;否则返回当前节点
        return (q != null)? q : p;
    } else {
        // 如果键小于等于当前节点的键,向左子树查找
        return lowerEntry(key, p.left);
    }
}

lowerEntry 方法用于获取小于指定键的最大键值对。它会调用 lowerEntry 方法从根节点开始查找,找到小于指定键的最大节点,然后调用 exportEntry 方法将节点导出为 Map.EntrylowerEntry 方法根据键和当前节点的键的大小关系,向左或向右子树递归查找。

4.6.4 higherEntry 方法
// 获取大于指定键的最小键值对
public Map.Entry<K,V> higherEntry(K key) {
    // 从根节点开始查找
    Entry<K,V> p = higherEntry(key, root);
    // 导出节点为 Map.Entry
    return exportEntry(p);
}

// 从指定节点开始查找大于指定键的最小节点
private Entry<K,V> higherEntry(K key, Entry<K,V> p) {
    // 如果节点为空,返回 null
    if (p == null)
        return null;
    // 比较键和当前节点的键的大小
    int cmp = compare(key, p.key);
    // 如果键小于当前节点的键
    if (cmp < 0) {
        // 向左子树查找
        Entry<K,V> q = higherEntry(key, p.left);
        // 如果左子树中找到的节点不为空,返回该节点;否则返回当前节点
        return (q != null)? q : p;
    } else {
        // 如果键大于等于当前节点的键,向右子树查找
        return higherEntry(key, p.right);
    }
}

higherEntry 方法用于获取大于指定键的最小键值对。它会调用 higherEntry 方法从根节点开始查找,找到大于指定键的最小节点,然后调用 exportEntry 方法将节点导出为 Map.EntryhigherEntry 方法根据键和当前节点的键的大小关系,向左或向右子树递归查找。

五、TreeMap 性能分析

5.1 时间复杂度分析

  • 插入操作TreeMap 的插入操作时间复杂度为 O(log n),其中 n 是树中元素的数量。这是因为红黑树是一种自平衡的二叉搜索树,插入操作需要从根节点开始遍历树,找到合适的插入位置,然后进行红黑树的平衡调整。在最坏情况下,树的高度为 O(log n),因此插入操作的时间复杂度为 O(log n)。
  • 查找操作:查找操作的时间复杂度同样为 O(log n)。查找时,从根节点开始,根据键的大小关系,向左或向右子树遍历,直到找到相同的键或遍历到叶子节点。由于红黑树的平衡性质,树的高度为 O(log n),所以查找操作的时间复杂度为 O(log n)。
  • 删除操作:删除操作的时间复杂度也是 O(log n)。删除操作需要先找到要删除的节点,然后进行节点的替换或直接删除,最后进行红黑树的平衡调整。整个过程的时间复杂度取决于树的高度,即 O(log n)。
  • 遍历操作:顺序遍历和逆序遍历的时间复杂度为 O(n),因为需要遍历树中的每个节点一次。
  • 范围查询操作:范围查询操作(如 subMapheadMaptailMap)的时间复杂度为 O(log n + m),其中 n 是树中元素的数量,m 是范围内元素的数量。首先需要找到范围的起始节点,时间复杂度为 O(log n),然后遍历范围内的元素,时间复杂度为 O(m)。

5.2 空间复杂度分析

TreeMap 的空间复杂度为 O(n),其中 n 是树中元素的数量。这是因为每个节点需要存储键、值、左右子节点、父节点和颜色信息,因此空间开销与元素数量成正比。

5.3 性能比较

HashMap 相比,TreeMap 的插入、查找和删除操作的时间复杂度较高,因为 HashMap 基于哈希表实现,平均时间复杂度为 O(1)。但是,TreeMap 具有有序性,能够根据键的顺序对元素进行排序,而 HashMap 不保证元素的顺序。

LinkedHashMap 相比,LinkedHashMap 可以保持插入顺序或访问顺序,而 TreeMap 是根据键的排序规则进行排序。LinkedHashMap 的插入、查找和删除操作的平均时间复杂度也为 O(1),而 TreeMap 为 O(log n)。

六、TreeMap 的线程安全性

6.1 非线程安全的原因

TreeMap 是非线程安全的,这是因为它的核心操作(如插入、删除和查找)没有进行同步处理。在多线程环境下,如果多个线程同时对 TreeMap 进行读写操作,可能会导致数据不一致、红黑树结构被破坏等问题。

例如,当一个线程正在进行插入操作,而另一个线程同时进行删除操作时,可能会导致红黑树的平衡被破坏,从而影响后续的操作。

6.2 线程安全的替代方案

如果需要在多线程环境下使用有序的 Map,可以考虑使用 ConcurrentSkipListMapConcurrentSkipListMap 是 Java 并发包中的一个类,它基于跳表(Skip List)数据结构实现,支持高并发的读写操作,并且能够保证元素的有序性。

以下是一个使用 ConcurrentSkipListMap 的示例:

import java.util.concurrent.ConcurrentSkipListMap;

public class ConcurrentSkipListMapExample {
    public static void main(String[] args) {
        // 创建一个 ConcurrentSkipListMap 实例
        ConcurrentSkipListMap<Integer, String> concurrentSkipListMap = new ConcurrentSkipListMap<>();

        // 向 ConcurrentSkipListMap 中添加键值对
        concurrentSkipListMap.put(3, "Apple");
        concurrentSkipListMap.put(1, "Banana");
        concurrentSkipListMap.put(2, "Cherry");

        // 遍历 ConcurrentSkipListMap,元素将按键的升序输出
        for (Integer key : concurrentSkipListMap.keySet()) {
            System.out.println("Key: " + key + ", Value: " + concurrentSkipListMap.get(key));
        }
    }
}

在上述示例中,我们创建了一个 ConcurrentSkipListMap 实例,并向其中添加了一些键值对。然后,我们遍历 ConcurrentSkipListMap,可以看到元素是按照键的升序输出的。

七、TreeMap 的序列化与反序列化

7.1 序列化机制

TreeMap 实现了 Serializable 接口,因此可以进行序列化和反序列化操作。在序列化时,TreeMap 会将其内部的红黑树结构和键值对信息保存到字节流中。

import java.io.*;
import java.util.TreeMap;

public class TreeMapSerializationExample {
    public static void main(String[] args) {
        // 创建一个 TreeMap 实例
        TreeMap<Integer, String> treeMap = new TreeMap<>();
        // 向 TreeMap 中添加键值对
        treeMap.put(1, "Apple");
        treeMap.put(2, "Banana");
        treeMap.put(3, "Cherry");

        try {
            // 创建一个文件输出流
            FileOutputStream fileOut = new FileOutputStream("treeMap.ser");
            // 创建一个对象输出流
            ObjectOutputStream out = new ObjectOutputStream(fileOut);
            // 将 TreeMap 写入对象输出流
            out.writeObject(treeMap);
            // 关闭对象输出流
            out.close();
            // 关闭文件输出流
            fileOut.close();
            System.out.println("Serialized data is saved in treeMap.ser");
        } catch (IOException i) {
            // 处理 IOException 异常
            i.printStackTrace();
        }
    }
}

在上述示例中,我们创建了一个 TreeMap 实例,并向其中添加了一些键值对。然后,我们使用 ObjectOutputStreamTreeMap 写入文件 treeMap.ser 中。

7.2 反序列化机制

在反序列化时,TreeMap 会从字节流中读取数据,并重新构建红黑树结构和键值对信息。

import java.io.*;
import java.util.TreeMap;

public class TreeMapDeserializationExample {
    public static void main(String[] args) {
        TreeMap<Integer, String> treeMap = null;
        try {
            // 创建一个文件输入流
            FileInputStream fileIn = new FileInputStream("treeMap.ser");
            // 创建一个对象输入流
            ObjectInputStream in = new ObjectInputStream(fileIn);
            // 从对象输入流中读取 TreeMap
            treeMap = (TreeMap<Integer, String>) in.readObject();
            // 关闭对象输入流
            in.close();
            // 关闭文件输入流
            fileIn.close();
        } catch (IOException i) {
            // 处理 IOException 异常
            i.printStackTrace();
            return;
        } catch (ClassNotFoundException c) {
            // 处理 ClassNotFoundException 异常
            System.out.println("TreeMap class not found");
            c.printStackTrace();
            return;
        }

        // 遍历反序列化后的 TreeMap
        for (Integer key : treeMap.keySet()) {
            System.out.println("Key: " + key + ", Value: " + treeMap.get(key));
        }
    }
}

在上述示例中,我们使用 ObjectInputStream 从文件 treeMap.ser 中读取 TreeMap,并将其反序列化。然后,我们遍历反序列化后的 TreeMap,输出键值对信息。

7.3 注意事项

  • 版本兼容性:在进行序列化和反序列化时,需要确保 TreeMap 的版本一致。如果序列化和反序列化的 TreeMap 版本不同,可能会导致反序列化失败。
  • 可序列化性TreeMap 中的键和值必须实现 Serializable 接口,否则会抛出 NotSerializableException 异常。

八、TreeMap 的使用场景

8.1 实现排行榜

由于 TreeMap 能够根据键的顺序对元素进行排序,因此可以很方便地实现排行榜功能。例如,在一个游戏中,根据玩家的分数进行排名。

import java.util.TreeMap;

public class LeaderboardExample {
    public static void main(String[] args) {
        // 创建一个 TreeMap 实例,键为分数,值为玩家姓名
        TreeMap<Integer, String> leaderboard = new TreeMap<>();

        // 向排行榜中添加玩家信息
        leaderboard.put(100, "Alice");
        leaderboard.put(80, "Bob");
        leaderboard.put(90, "Charlie");

        // 输出排行榜,分数从低到高
        for (Integer score : leaderboard.keySet()) {
            System.out.println("Score: " + score + ", Player: " + leaderboard.get(score));
        }

        // 输出排行榜,分数从高到低
        for (Integer score : leaderboard.descendingKeySet()) {
            System.out.println("Score: " + score + ", Player: " + leaderboard.get(score));
        }
    }
}

在上述示例中,我们创建了一个 TreeMap 实例,键为分数,值为玩家姓名。然后,我们向排行榜中添加了一些玩家信息,并分别按分数从低到高和从高到低的顺序输出排行榜。

8.2 区间查询

TreeMap 的范围查询功能(如 subMapheadMaptailMap)使其非常适合进行区间查询。例如,在一个时间序列数据中,查询某个时间段内的数据。

import java.util.TreeMap;

public class RangeQueryExample {
    public static void main(String[] args) {
        // 创建一个 TreeMap 实例,键为时间,值为数据
        TreeMap<Integer, String> timeSeriesData = new TreeMap<>();

        // 向时间序列数据中添加数据
        timeSeriesData.put(1, "Data 1");
        timeSeriesData.put(2, "Data 2");
        timeSeriesData.put(3, "Data 3");
        timeSeriesData.put(4, "Data 4");
        timeSeriesData.put(5, "Data 5");

        // 查询时间范围 [2, 4] 内的数据
        TreeMap<Integer, String> subMap = (TreeMap<Integer, String>) timeSeriesData.subMap(2, true, 4, true);

        // 输出查询结果
        for (Integer time : subMap.keySet()) {
            System.out.println("Time: " + time + ", Data: " + subMap.get(time));
        }
    }
}

在上述示例中,我们创建了一个 TreeMap 实例,键为时间,值为数据。然后,我们使用 subMap 方法查询时间范围 [2, 4] 内的数据,并输出查询结果。

8.3 实现 LRU 缓存(结合 LinkedHashMap)

虽然 TreeMap 本身不适合直接实现 LRU(Least Recently Used)缓存,但可以结合 LinkedHashMap 来实现。LinkedHashMap 可以保持插入顺序或访问顺序,通过重写 removeEldestEntry 方法,可以实现 LRU 缓存的淘汰机制。

import java.util.LinkedHashMap;
import java.util.Map;

// 自定义 LRU 缓存类,继承自 LinkedHashMap
class LRUCache<K, V> extends LinkedHashMap<K, V> {
    // 缓存容量
    private final int capacity;

    // 构造函数,初始化缓存容量
    public LRUCache(int capacity) {
        // 调用父类构造函数,设置初始容量、负载因子和访问顺序
        super(capacity, 0.75f, true) {
            @Override
            // 重写 removeEldestEntry 方法,当缓存大小超过容量时,移除最旧的元素
            protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
                return size() > capacity;
            }
        };
        this.capacity = capacity;
    }
}

public class LRUCacheExample {
    public static void main(String[] args) {
        // 创建一个 LRU 缓存实例,容量为 3
        LRUCache<Integer, String> cache = new LRUCache<>(3);

        // 向缓存中添加元素
        cache.put(1, "Apple");
        cache.put(2, "Banana");
        cache.put(3, "Cherry");

        // 访问元素 2
        System.out.println(cache.get(2));

        // 向缓存中添加元素 4,此时缓存已满,会移除最旧的元素 1
        cache.put(4, "Date");

        // 输出缓存中的元素
        for (Integer key : cache.keySet()) {
            System.out.println("Key: " + key + ", Value: " + cache.get(key));
        }
    }
}

在上述示例中,我们自定义了一个 LRUCache 类,继承自 LinkedHashMap。通过重写 removeEldestEntry 方法,当缓存大小超过容量时,会移除最旧的元素。然后,我们创建了一个 LRUCache 实例,并进行了元素的添加、访问和输出操作。

九、TreeMap 与其他集合类的比较

9.1 与 HashMap 的比较

  • 有序性HashMap 不保证元素的顺序,而 TreeMap 会根据键的顺序对元素进行排序。如果需要有序的存储和访问,TreeMap 是更好的选择。
  • 性能HashMap 的插入、查找和删除操作的平均时间复杂度为 O(1),而 TreeMap 为 O(log n)。因此,在对性能要求较高且不需要有序性的场景下,HashMap 更合适。
  • 内存开销HashMap 的内存开销相对较小,因为它只需要维护一个哈希表。而 TreeMap 需要维护一个红黑树结构,内存开销相对较大。

9.2 与 LinkedHashMap 的比较

  • 顺序规则LinkedHashMap 可以保持插入顺序或访问顺序,而 TreeMap 是根据键的排序规则进行排序。如果需要根据插入或访问顺序进行操作,LinkedHashMap 更合适;如果需要根据键的排序进行操作,TreeMap 更合适。
  • 性能LinkedHashMap 的插入、查找和删除操作的平均时间复杂度为 O(1),而 TreeMap 为 O(log n)。因此,在对性能要求较高的场景下,LinkedHashMap 更有优势。

9.3 与 Hashtable 的比较

  • 线程安全性Hashtable 是线程安全的,而 TreeMap 是非线程安全的。如果需要在多线程环境下使用,Hashtable 是一个选择;但如果需要有序性,可以考虑使用 ConcurrentSkipListMap
  • 性能Hashtable 的性能相对较低,因为它的所有方法都进行了同步处理。而 TreeMap 的性能在非线程安全的情况下较好。
  • 键和值的限制Hashtable 不允许键和值为 null,而 TreeMap 允许键和值为 null,但键为 null 时需要使用自定义的比较器。

十、TreeMap 的优化建议

10.1 合理选择比较器

  • 自然顺序:如果键实现了 Comparable 接口,并且希望使用键的自然顺序进行排序,可以不传入比较器,TreeMap 会默认使用键的自然顺序。
  • 自定义比较器:如果需要根据特定的规则进行排序,可以传入自定义的比较器。例如,在一个存储学生信息的 TreeMap 中,根据学生的年龄进行排序:
import java.util.Comparator;
import java.util.TreeMap;

// 学生类
class Student {
    // 学生姓名
    String name;
    // 学生年龄
    int age;

    // 构造函数,初始化学生姓名和年龄
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 获取学生姓名
    public String getName() {
        return name;
    }

    // 获取学生年龄
    public int getAge() {
        return age;
    }
}

// 学生年龄比较器
class StudentAgeComparator implements Comparator<Student> {
    // 比较两个学生的年龄
    @Override
    public int compare(Student s1, Student s2) {
        return s1.getAge() - s2.getAge();
    }
}

public class CustomComparatorExample {
    public static void main(String[] args) {
        // 创建一个 TreeMap 实例,使用自定义的学生年龄比较器
        TreeMap<Student, String> studentMap = new TreeMap<>(new StudentAgeComparator());

        // 向 TreeMap 中添加学生信息
        studentMap.put(new Student("Alice", 20), "Grade A");
        studentMap.put(new Student("Bob", 18), "Grade B");
        studentMap.put(new Student("Charlie", 22), "Grade C");

        // 遍历 TreeMap,学生将按年龄升序输出
        for (Student student : studentMap.keySet()) {
            System.out.println("Name: " + student.getName() + ", Age: " + student.getAge() + ", Grade: " + studentMap.get(student));
        }
    }
}

在上述示例中,我们定义了一个 Student 类和一个 StudentAgeComparator 类,StudentAgeComparator 实现了 Comparator 接口,用于比较学生的年龄。然后,我们创建了一个 TreeMap 实例,使用 StudentAgeComparator 作为比较器,向 TreeMap 中添加学生信息,并按年龄升序输出。

10.2 避免频繁的扩容操作

虽然 TreeMap 不像 HashMap 那样有扩容的概念,但在插入大量元素时,红黑树的平衡调整会带来一定的性能开销。因此,可以在创建 TreeMap 时,根据预估的元素数量,合理分配内存,减少平衡调整的次数。

10.3 优化哈希函数(如果适用)

虽然 TreeMap 不使用哈希函数,但如果键是自定义类,并且需要使用自定义的比较器,应该确保比较器的实现高效。比较器的实现应该尽量减少不必要的计算,提高比较的速度。

10.4 减少不必要的遍历操作

在使用 TreeMap 时,应该尽量减少不必要的遍历操作。例如,如果只需要获取第一个或最后一个元素,使用 firstEntrylastEntry 方法,而不是遍历整个 TreeMap

十一、总结与展望

11.1 总结

通过对 TreeMap 的深入分析,我们全面了解了它的内部结构、核心操作原理、性能特点、线程安全性、序列化机制以及使用场景等方面的内容。TreeMap 基于红黑树数据结构实现,能够根据键的顺序对元素进行排序,提供了高效的插入、查找、删除和范围查询操作。

在内部结构上,TreeMap 包含一个红