一、TreeMap有两种排序方式
- 自然排序(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}
- 定制排序(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;
}
- 如果树为空,直接创建一个根节点并返回。
- 如果使用自定义比较器,则使用
comparator.compare
方法比较键;否则使用键对象自身的compareTo
方法比较。 - 沿着树从根节点开始遍历,根据比较结果选择左子树还是右子树,直到找到插入位置或遇到重复键。
- 如果找到重复键,直接更新其值并返回旧值。
- 如果未找到重复键,创建新节点,并将其链接到父节点中合适的位置。
- 调用fixAfterInsertion方法调整树结构,维护平衡。
- 更新size大小和modCount计数器。
需要注意的几点:
- 如果使用自然排序,键对象必须实现Comparable接口,否则会抛出ClassCastException。
- 如果键为null且使用自然排序,会抛出NullPointerException。
- 在比较时使用了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
接口。
解决方案:
- 让
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;
}
-
首先检查是否使用了自定义比较器。如果使用了,则调用getEntryUsingComparator方法,该方法的实现类似,只是使用comparator.compare方法进行比较。
-
如果使用自然排序,并且传入的键为null,则抛出NullPointerException。
-
获取键对象的Comparable视图,用于后续比较。
-
从根节点开始遍历树。使用键对象的compareTo方法与当前节点的键进行比较,根据比较结果选择左子树还是右子树继续遍历。
-
如果找到与键对象相等的节点,直接返回该节点。
-
如果遍历完整棵树还未找到对应节点,返回null。
六、总结
-
数据结构
- TreeMap底层使用红黑树(Red-Black Tree),和 HashMap 的红黑树结构一样。红黑树是一种自平衡的二叉查找树。
- 每个节点(Entry)存储键值对数据,并通过左右子节点指针维护树的结构。
-
排序规则
- TreeMap支持两种排序方式:自然排序(Natural Order)和自定义排序(Custom Order)。
- 自然排序要求键对象实现Comparable接口,并根据对象的compareTo方法进行比较。
- 自定义排序通过在构造时传入Comparator对象来指定排序规则。