在 Java 中,TreeMap
和 LinkedHashMap
是非常常用的集合类,它们在存储元素、顺序控制以及性能方面各有特点。通过了解它们的底层实现和使用场景,可以帮助我们更好地选择合适的数据结构。在本文中,我们将深入剖析这两者的基本原理、源码实现以及使用场景,帮助你在开发中做出明智选择。
一、TreeMap:基于红黑树的有序映射
1.1 TreeMap 的基本结构
TreeMap
是 Java 中实现 NavigableMap
接口的一个类,它的底层实现基于 红黑树。红黑树是一种自平衡的二叉查找树,它能够高效地实现对元素的查找、插入和删除操作。在 TreeMap
中,元素是根据 键的自然顺序 或者 外部比较器 进行排序的。
TreeMap
的元素的插入和查找时间复杂度为 O(log n),因此它适用于需要根据 键排序 的场景。
private final Comparator<? super K> comparator;
private transient Entry<K,V> root;
private transient int size = 0;
private transient int modCount = 0;
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;
}
1.2 TreeMap
的 put
方法
TreeMap
的 put
方法根据红黑树的性质,依照键的大小关系,将新的键值对插入到树中。插入时,会根据键值使用 外部比较器 或者 内部的 compareTo
方法 来确定新节点应该放置的位置。如果树中已经存在相同的键,则更新对应的值。
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
} else {
if (key == null) throw new NullPointerException();
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);
size++;
modCount++;
return null;
}
1.3 TreeMap
的特点
- 排序方式:
TreeMap
按照键的自然顺序或提供的比较器进行排序。 - 时间复杂度:插入、删除、查找的时间复杂度都是 O(log n)。
- 键不可为 null:
TreeMap
不允许null
键,如果尝试插入null
键会抛出NullPointerException
。
二、LinkedHashMap:保持插入顺序或访问顺序的 HashMap
2.1 LinkedHashMap 的基本结构
LinkedHashMap
继承自 HashMap
,它通过在每个元素之间维护 双向链表 来维护元素的顺序。通过这种方式,LinkedHashMap
可以按 插入顺序 或 访问顺序 来迭代元素。LinkedHashMap
支持两种顺序:
- 插入顺序:元素的插入顺序保持不变,修改操作不影响顺序。
- 访问顺序:每次访问元素时,该元素会被移动到链表的末尾,从而实现最近最常访问的元素排在链表末尾。
2.2 插入顺序与访问顺序
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("1", "one");
map.put("2", "two");
map.put("3", "three");
System.out.println(map); // 输出:{1=one, 2=two, 3=three}
map.get("2");
System.out.println(map); // 输出:{1=one, 3=three, 2=two} (访问顺序改变)
2.3 LinkedHashMap
适配 LRU 缓存
LinkedHashMap
还可以被用来实现一个简单的 LRU(最近最少使用)缓存。通过重写 removeEldestEntry
方法,当元素超过缓存最大容量时,可以自动移除最久未使用的元素。
LinkedHashMap<String, String> map = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 3;
}
};
map.put("1", "one");
map.put("2", "two");
map.put("3", "three");
map.get("1");
map.put("4", "four");
System.out.println(map); // 输出:{3=three, 1=one, 4=four} (自动移除最久未访问的元素)
2.4 LinkedHashMap 的基本属性
LinkedHashMap
类中有两个重要的属性用于管理双向链表:
static class Entry<K, V> extends HashMap.Node<K, V> {
Entry<K,V> before, after;
}
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
final boolean accessOrder;
head
和tail
:分别表示双向链表的头部和尾部。accessOrder
:控制顺序的方式,true
表示按访问顺序排序,false
表示按插入顺序排序。
2.5 LinkedHashMap
关键方法
put
和get
方法:通过继承HashMap
,这些方法可以正常使用,同时可以在访问时更新元素的顺序。afterNodeAccess
:当元素被访问时,更新元素在链表中的位置。afterNodeInsertion
:在元素插入后,如果需要移除最久未使用的元素,会调用removeEldestEntry
方法。
三、总结
3.1 TreeMap
与 LinkedHashMap
的比较
特性 | TreeMap | LinkedHashMap |
---|---|---|
底层数据结构 | 红黑树 (自平衡二叉查找树) | 哈希表 + 双向链表 |
元素排序 | 有序,按键的自然顺序或自定义比较器 | 无序(插入顺序)或按访问顺序(通过 accessOrder 控制) |
时间复杂度 | O(log n) | O(1) |
null 键 | 不允许 | 允许 null 键 |
使用场景 | 需要排序的键值对 | 需要保持元素顺序(插入或访问顺序) |
3.2 使用建议
TreeMap
适合用在需要排序的场景,比如需要按照键值排序、查找范围元素等。LinkedHashMap
适合用在需要维护元素插入顺序或者访问顺序的场景,尤其是需要实现 LRU 缓存 时非常有用。
了解这两者的底层实现和应用场景,可以帮助我们在开发中做出更合适的选择,充分利用它们各自的优势。