女排世界杯_1966世界杯 - ezrjnk120.com

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

2025-12-21 06:45:30

深度剖析 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 提供了丰富的方法来进行范围查询,如 subMap、headMap 和 tailMap 等,可以方便地获取指定范围内的键值对。

2.3 基本使用示例

import java.util.TreeMap;

public class TreeMapExample {

public static void main(String[] args) {

// 创建一个 TreeMap 实例,使用键的自然顺序进行排序

TreeMap 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,可以看到元素是按照键的升序输出的。最后,我们使用 firstEntry 和 lastEntry 方法分别获取了第一个和最后一个键值对。

三、TreeMap 源码结构分析

3.1 类的定义与继承关系

// TreeMap 类继承自 AbstractMap 类,并实现了 NavigableMap 接口

public class TreeMap

extends AbstractMap

implements NavigableMap, Cloneable, java.io.Serializable

{

// 比较器,用于指定键的排序规则,如果为 null,则使用键的自然顺序

private final Comparator comparator;

// 红黑树的根节点

private transient Entry root;

// 树中元素的数量

private transient int size = 0;

// 记录结构修改次数,用于快速失败机制

private transient int modCount = 0;

// 构造函数,使用键的自然顺序进行排序

public TreeMap() {

comparator = null;

}

// 构造函数,使用指定的比较器进行排序

public TreeMap(Comparator comparator) {

this.comparator = comparator;

}

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

}

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

3.2 节点类的定义

// 静态内部类 Entry,用于表示红黑树的节点

static final class Entry implements Map.Entry {

// 键

K key;

// 值

V value;

// 左子节点

Entry left;

// 右子节点

Entry right;

// 父节点

Entry parent;

// 节点的颜色,true 表示红色,false 表示黑色

boolean color = BLACK;

// 构造函数,初始化键、值和父节点

Entry(K key, V value, Entry 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 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 parent;

// split comparator and comparable paths

// 获取比较器

Comparator 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 k = (Comparable) 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 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)k1).compareTo((K)k2)

: comparator.compare((K)k1, (K)k2);

}

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

4.1.2 fixAfterInsertion 方法

// 插入新节点后,进行红黑树的平衡调整

private void fixAfterInsertion(Entry x) {

// 新插入的节点颜色设为红色

x.color = RED;

// 当节点不为 null 且不是根节点,且父节点颜色为红色时

while (x != null && x != root && x.parent.color == RED) {

// 如果父节点是祖父节点的左子节点

if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {

// 获取叔叔节点

Entry 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 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 Entry parentOf(Entry p) {

return (p == null? null: p.parent);

}

// 获取节点的左子节点

private static Entry leftOf(Entry p) {

return (p == null)? null: p.left;

}

// 获取节点的右子节点

private static Entry rightOf(Entry p) {

return (p == null)? null: p.right;

}

// 获取节点的颜色

private static boolean colorOf(Entry p) {

return (p == null? BLACK : p.color);

}

// 设置节点的颜色

private static void setColor(Entry p, boolean c) {

if (p != null)

p.color = c;

}

// 左旋操作

private void rotateLeft(Entry p) {

if (p != null) {

// 获取 p 的右子节点

Entry 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 p) {

if (p != null) {

// 获取 p 的左子节点

Entry 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 方法用于在插入新节点后进行红黑树的平衡调整,以保证红黑树的性质。红黑树有以下几个重要性质:

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

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

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

4.2 查找元素操作

4.2.1 get 方法

// 根据键获取对应的值

public V get(Object key) {

// 调用 getEntry 方法查找节点

Entry p = getEntry(key);

// 如果找到节点,返回其值;否则返回 null

return (p==null? null : p.value);

}

// 根据键查找节点

final Entry getEntry(Object key) {

// 如果比较器不为 null,调用 getEntryUsingComparator 方法查找节点

if (comparator != null)

return getEntryUsingComparator(key);

// 如果键为 null,抛出 NullPointerException

if (key == null)

throw new NullPointerException();

@SuppressWarnings("unchecked")

// 将键转换为 Comparable 类型

Comparable k = (Comparable) key;

// 从根节点开始遍历树

Entry 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 getEntryUsingComparator(Object key) {

@SuppressWarnings("unchecked")

// 将键转换为 K 类型

K k = (K) key;

// 获取比较器

Comparator cpr = comparator;

// 从根节点开始遍历树

if (cpr != null) {

Entry 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 方法查找节点,如果找到节点,则返回其值;否则返回 null。getEntry 方法根据比较器的情况,选择使用 getEntryUsingComparator 方法或键的自然顺序进行查找。查找过程从根节点开始,根据键的大小关系,向左或向右子树遍历,直到找到相同的键或遍历到叶子节点。

4.3 删除元素操作

4.3.1 remove 方法

// 根据键移除对应的键值对

public V remove(Object key) {

// 调用 getEntry 方法查找节点

Entry p = getEntry(key);

// 如果未找到节点,返回 null

if (p == null)

return null;

// 保存节点的值

V oldValue = p.value;

// 调用 deleteEntry 方法删除节点

deleteEntry(p);

// 返回旧的值

return oldValue;

}

// 删除节点

private void deleteEntry(Entry 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 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 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 TreeMap.Entry successor(Entry t) {

// 如果节点为空,返回 null

if (t == null)

return null;

// 如果节点有右子节点

else if (t.right != null) {

// 从右子节点开始,一直向左子树遍历,找到最小的节点

Entry p = t.right;

while (p.left != null)

p = p.left;

return p;

} else {

// 如果节点没有右子节点,向上查找,直到找到一个节点是其父节点的左子节点

Entry p = t.parent;

Entry ch = t;

while (p != null && ch == p.right) {

ch = p;

p = p.parent;

}

return p;

}

}

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

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

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

4.3.2 fixAfterDeletion 方法

// 删除节点后,进行红黑树的平衡调整

private void fixAfterDeletion(Entry x) {

// 当节点不为根节点且颜色为黑色时

while (x != root && colorOf(x) == BLACK) {

// 如果节点是其父节点的左子节点

if (x == leftOf(parentOf(x))) {

// 获取兄弟节点

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

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

4.4 遍历操作

4.4.1 迭代器的实现

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

// KeySet 类,继承自 AbstractSet 类

final class KeySet extends AbstractSet {

// 获取迭代器

public Iterator 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 {

// 构造函数,初始化迭代器

KeyIterator(Entry first) {

super(first);

}

// 获取下一个键

public K next() {

return nextEntry().key;

}

}

// 私有条目迭代器类

abstract class PrivateEntryIterator implements Iterator {

// 下一个要访问的节点

Entry next;

// 最后访问的节点

Entry lastReturned;

// 记录结构修改次数

int expectedModCount;

// 构造函数,初始化迭代器

PrivateEntryIterator(Entry first) {

// 记录结构修改次数

expectedModCount = modCount;

// 最后访问的节点置为 null

lastReturned = null;

// 下一个要访问的节点设为第一个节点

next = first;

}

// 判断是否还有下一个元素

public final boolean hasNext() {

return next != null;

}

4.4 遍历操作(续)

4.4.1 迭代器的实现(续)

// 获取下一个条目

final Entry nextEntry() {

// 获取下一个要访问的节点

Entry 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 prevEntry() {

// 获取下一个要访问的节点

Entry 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 TreeMap.Entry predecessor(Entry t) {

// 如果节点为空,返回 null

if (t == null)

return null;

// 如果节点有左子节点

else if (t.left != null) {

// 从左子节点开始,一直向右子树遍历,找到最大的节点

Entry p = t.left;

while (p.right != null)

p = p.right;

return p;

} else {

// 如果节点没有左子节点,向上查找,直到找到一个节点是其父节点的右子节点

Entry p = t.parent;

Entry 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 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 ck = (Comparable) fromKey;

ck.compareTo(toKey);

}

// 创建一个 SubMap 实例

return new SubMap<>(this,

false, fromKey, fromInclusive,

false, toKey, toInclusive);

}

// SubMap 类,继承自 AbstractMap 类,并实现了 NavigableMap 接口

static final class SubMap extends AbstractMap

implements NavigableMap, java.io.Serializable

{

// 父 TreeMap

private final TreeMap 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 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 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 实例,其中 fromStart 为 true,表示从起始开始,fromKey 为 null,toKey 为指定的键,inclusive 表示是否包含结束键。

4.5.3 tailMap 方法

// 获取大于等于指定键的子 TreeMap

public NavigableMap 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 实例,其中 fromStart 为 false,fromKey 为指定的键,inclusive 表示是否包含起始键,toEnd 为 true,表示到结束,toKey 为 null。

4.6 其他操作

4.6.1 firstEntry 方法

// 获取第一个键值对

public Map.Entry firstEntry() {

// 获取第一个节点

return exportEntry(getFirstEntry());

}

// 获取第一个节点

final Entry getFirstEntry() {

// 从根节点开始,一直向左子树遍历,找到最小的节点

Entry p = root;

if (p != null)

while (p.left != null)

p = p.left;

return p;

}

// 导出节点为 Map.Entry

static Map.Entry exportEntry(TreeMap.Entry e) {

// 如果节点为空,返回 null

return (e == null)? null :

new AbstractMap.SimpleImmutableEntry<>(e);

}

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

4.6.2 lastEntry 方法

// 获取最后一个键值对

public Map.Entry lastEntry() {

// 获取最后一个节点

return exportEntry(getLastEntry());

}

// 获取最后一个节点

final Entry getLastEntry() {

// 从根节点开始,一直向右子树遍历,找到最大的节点

Entry p = root;

if (p != null)

while (p.right != null)

p = p.right;

return p;

}

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

4.6.3 lowerEntry 方法

// 获取小于指定键的最大键值对

public Map.Entry lowerEntry(K key) {

// 从根节点开始查找

Entry p = lowerEntry(key, root);

// 导出节点为 Map.Entry

return exportEntry(p);

}

// 从指定节点开始查找小于指定键的最大节点

private Entry lowerEntry(K key, Entry p) {

// 如果节点为空,返回 null

if (p == null)

return null;

// 比较键和当前节点的键的大小

int cmp = compare(key, p.key);

// 如果键大于当前节点的键

if (cmp > 0) {

// 向右子树查找

Entry q = lowerEntry(key, p.right);

// 如果右子树中找到的节点不为空,返回该节点;否则返回当前节点

return (q != null)? q : p;

} else {

// 如果键小于等于当前节点的键,向左子树查找

return lowerEntry(key, p.left);

}

}

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

4.6.4 higherEntry 方法

// 获取大于指定键的最小键值对

public Map.Entry higherEntry(K key) {

// 从根节点开始查找

Entry p = higherEntry(key, root);

// 导出节点为 Map.Entry

return exportEntry(p);

}

// 从指定节点开始查找大于指定键的最小节点

private Entry higherEntry(K key, Entry p) {

// 如果节点为空,返回 null

if (p == null)

return null;

// 比较键和当前节点的键的大小

int cmp = compare(key, p.key);

// 如果键小于当前节点的键

if (cmp < 0) {

// 向左子树查找

Entry q = higherEntry(key, p.left);

// 如果左子树中找到的节点不为空,返回该节点;否则返回当前节点

return (q != null)? q : p;

} else {

// 如果键大于等于当前节点的键,向右子树查找

return higherEntry(key, p.right);

}

}

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

五、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),因为需要遍历树中的每个节点一次。范围查询操作:范围查询操作(如 subMap、headMap 和 tailMap)的时间复杂度为 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,可以考虑使用 ConcurrentSkipListMap。ConcurrentSkipListMap 是 Java 并发包中的一个类,它基于跳表(Skip List)数据结构实现,支持高并发的读写操作,并且能够保证元素的有序性。

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

import java.util.concurrent.ConcurrentSkipListMap;

public class ConcurrentSkipListMapExample {

public static void main(String[] args) {

// 创建一个 ConcurrentSkipListMap 实例

ConcurrentSkipListMap 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 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 实例,并向其中添加了一些键值对。然后,我们使用 ObjectOutputStream 将 TreeMap 写入文件 treeMap.ser 中。

7.2 反序列化机制

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

import java.io.*;

import java.util.TreeMap;

public class TreeMapDeserializationExample {

public static void main(String[] args) {

TreeMap treeMap = null;

try {

// 创建一个文件输入流

FileInputStream fileIn = new FileInputStream("treeMap.ser");

// 创建一个对象输入流

ObjectInputStream in = new ObjectInputStream(fileIn);

// 从对象输入流中读取 TreeMap

treeMap = (TreeMap) 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 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 的范围查询功能(如 subMap、headMap 和 tailMap)使其非常适合进行区间查询。例如,在一个时间序列数据中,查询某个时间段内的数据。

import java.util.TreeMap;

public class RangeQueryExample {

public static void main(String[] args) {

// 创建一个 TreeMap 实例,键为时间,值为数据

TreeMap 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 subMap = (TreeMap) 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 extends LinkedHashMap {

// 缓存容量

private final int capacity;

// 构造函数,初始化缓存容量

public LRUCache(int capacity) {

// 调用父类构造函数,设置初始容量、负载因子和访问顺序

super(capacity, 0.75f, true) {

@Override

// 重写 removeEldestEntry 方法,当缓存大小超过容量时,移除最旧的元素

protected boolean removeEldestEntry(Map.Entry eldest) {

return size() > capacity;

}

};

this.capacity = capacity;

}

}

public class LRUCacheExample {

public static void main(String[] args) {

// 创建一个 LRU 缓存实例,容量为 3

LRUCache 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 {

// 比较两个学生的年龄

@Override

public int compare(Student s1, Student s2) {

return s1.getAge() - s2.getAge();

}

}

public class CustomComparatorExample {

public static void main(String[] args) {

// 创建一个 TreeMap 实例,使用自定义的学生年龄比较器

TreeMap 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 时,应该尽量减少不必要的遍历操作。例如,如果只需要获取第一个或最后一个元素,使用 firstEntry 或 lastEntry 方法,而不是遍历整个 TreeMap。

十一、总结与展望

11.1 总结

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

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