一、TreeMap的介绍
TreeMap继承自AbstractMap,实现了NavigableMap,基于红黑树实现,它内部的键值对映射是按照类内部指定的比较器Comparator进行排序,如果内部的排序器为空则根据键值对映射中Key的自然顺序进行排序,取决于实例化对象时使用的构造函数。Treemap的底层红黑树结构保证了containKey、get、put、remove方法定位Key操作的时间复杂度为O(logN)。TreeMap是非线程安全的,在创建容器迭代器Iterator之后如果有其他线程对容器做出结构性修改(添加、插入、删除键值对均属于结构性修改,修改已存在键值对的value值不算结构性修改)会抛出一个ConcurrentModificationException异常。
二、TreeMap的数据结构
1 – 继承结构
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
从源码中可以看出TreeMap继承AbstractMap,AbstractMap实现了Map的一些基本操作,此外是实现了NavigableMap、Cloneable、java.io.Serializable接口。NavigableMap接口定义了一系列基于排序的导航方法例如返回lowerEntry(K key)返回key小于key且排序最接近key的对应键值对,因为间接实现了SortedMap故TreeMap的键值对按序存储。
2 – 内部成员变量和底层数据结构
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
//key比较器
private final Comparator<? super K> comparator;
//根节点
private transient Entry<K,V> root;
//容器存储的节点个数
private transient int size = 0;
//结构化修改的次数
private transient int modCount = 0;
}
其他成员变量直接看源码注释就可以了,下面我看来看下根节点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;
//红黑树颜色
boolean color = BLACK;
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
//获取节点key
public K getKey() {
return key;
}
//获取键值对映射节点的value值
public V getValue() {
return value;
}
//设置键值对映射节点的value值
public V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
//节点比较
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());
}
public int hashCode() {
int keyHash = (key==null ? 0 : key.hashCode());
int valueHash = (value==null ? 0 : value.hashCode());
return keyHash ^ valueHash;
}
public String toString() {
return key + "=" + value;
}
}
查看Entry类的内部属性我们知道TreeMap底层是基于红黑树实现的,这里对于红黑树这种数据结构暂时不做详细分析,只要知道这是一种大致平衡的二叉树,且查询所需的时间复杂度为O(logN)
三、TreeMap的源码解析
1 – 构造函数
public TreeMap() {
comparator = null;
}
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m);
}
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) {
}
}
我们依次分析一下TreeMap提供的四种构造函数。
1)TreeMap()
没做啥,把排序器comparator设置为空,comparator被final修饰且之后键值对映射排序之前需要判断comparator是否为空。
2)TreeMap(Comparator<? super K> comparator)
将内部的key比较器设置为方法指定的比较器comparator
3)TreeMap(Map<? extends K, ? extends V> m)
方法入参是一个Map对象因为Map内部键值对存储不是有序的,不包含一个比较器comparator,将内部的比较器设置为null,把map对象包含的所有键值对按照Key实现的自然顺序插入TreeMap
4)TreeMap(SortedMap<K, ? extends V> m)
设置TreeMap内部比较器为SortedMap对象m内部指定的的比较器。然后按序遍历m中的所有键值对节点插入到TreeMap
2 – V get(Object key)根据key获取键值对映射value
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
get方法内部调用getEntry方法根据key获取TreeMap容器中保存的键值对节点p,然后判断节点p是否为空,为空返回null,不为空返回节点的value属性。我们继续跟进getEntry(key)方法源码看下该方法内部做了什么。
final Entry<K,V> getEntry(Object key) {
//内部比较器comparator不为空,基于它指定的比较器遍历红黑树获取key对应节点
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
//遍历TreeMap内部红黑树,基于指定对象key定义的比较函数比较指定key与节点key,获取指定key对应的节点信息
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
//指定key小于当前节点,继续遍历当前节点的左孩子节点
p = p.left;
else if (cmp > 0)
//指定key大于当前节点,继续遍历当前节点的右孩子节点
p = p.right;
else
//相等直接返回当前节点
return p;
}
return null;
}
方法内部首先判断内部比较器comparator是否为null,不为null直接调用getEntryUsingComparator(key)使用内部指定的比较器遍历比较TreeMap内部红黑树中的键值对节点key与指定key获取指定key匹配的节点信息,我们跟进该方法看下源码层面如何实现
final Entry<K,V> getEntryUsingComparator(Object key) {
@SuppressWarnings("unchecked")
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;
}
}
return null;
}
该方法内部实现逻辑很简单,就是从根节点开始遍历内部红黑树,基于内部比较器比较指定key与当前节点的key,相等直接返回当前节点,如果指定key小于当前节点key继续遍历当前节点的左节点,否则继续遍历当前节点的右节点。遍历完成还没找到指定key对应节点信息或者内部比较器comparator为null则方法返回null。
返回上一个方法getEntry继续分析。接下来对方法入参key进行判空,如果对象为null抛出空指针异常。接下来遍历TreeMap内部红黑树从根节点开始使用指定key类内部实现的compare方法(即类自然顺序)对两个对象key和红黑树当前节点key比较,若等于表示匹配上返回当前节点,小于继续遍历左孩子节点,大于则遍历右孩子节点。
3 – V put(K key, V value)插入键值对
public V put(K key, V value) {
Entry<K,V> t = root;
//若根节点为空
if (t == null) {
//类型校验可能为null
compare(key, key); // type (and possibly null) check
//基于键值对创建新的根节点
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
//根节点不为空继续执行后续代码
int cmp;
Entry<K,V> parent;
Comparator<? super K> cpr = comparator;
//若容器内部存在比较器,则遍历红黑树基于比较器comparator比较节点key和指定key获取指定键值对的插入节点,若该
//位置已经存在,则用新值value取代旧值
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);
}
//若比较器为空则使用指定key所属类实现的Comparable中定义的自然顺序遍历容器内部红黑树,比较节点key和指定key
//获取指定键值对的插入节点,若指定key已存在则用新值value覆盖旧值
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
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);
}
//创建新节点,设置双亲节点parent
Entry<K,V> e = new Entry<>(key, value, parent);
//若新插入节点的逻辑顺序小于双亲节点,则它被设置为双亲节点的左孩子
if (cmp < 0)
parent.left = e;
//否则设置为双亲节点的右孩子
else
parent.right = e;
//执行插入修正操作,保证在插入新节点之后仍然是红黑树,这里的调整操作主要包括两种1修改节点颜色;2对某些节点进
//行旋转进行旋转
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
4 – V remove(Object key)删除key对应的键值对
public V remove(Object key) {
Entry<K,V> p = getEntry(key);
if (p == null)
return null;
V oldValue = p.value;
deleteEntry(p);
return oldValue;
}
方法内部首先调用getEntry方法获取key对应的键值对节点,接着进行判空,如果返回的节点为空方法结束返回null,否则调用deleteEntry方法删除节点,返回删除节点的value。我们进入deleteEntry方法源码看下它是如何实现的。
private void deleteEntry(Entry<K,V> p) {
//为什么结构修改计数器和键值对个数size放在方法开头更新?
modCount++;
size--;
//待删除节点左右孩子节点均不为空
if (p.left != null && p.right != null) {
//获取指定节点的后继节点
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
}
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
//若删除节点左节点或者右节点不为空不为空
if (replacement != null) {
//替换节点的双亲节点指向被删除节点的双亲节点
replacement.parent = p.parent;
//被删除节点是根节点则重置根节点为替换节点
if (p.parent == null)
root = replacement;
//若被删除节点是他双亲节点的左孩子则让被删除节点的左孩子节点指向当前替换节点replacement
else if (p == p.parent.left)
p.parent.left = replacement;
//若被删除节点为双亲节点的右孩子则让被删除节点的右孩子节点指向当前节点replacement
else
p.parent.right = replacement;
// Null out links so they are OK to use by fixAfterDeletion.
p.left = p.right = p.parent = null;
//如果被删除的节点是黑色则需要执行删除后修复,保证删除节点后底层存储结构仍然是红黑树
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) {//若被删除节点是根节点,且左右孩子节点均为空,则根节点置空
root = null;
} else { //被删除节点不是根节点且左右子树均为空,因为红黑树需要满足一个节点到它的子孙节点的所有路径都包含相
//同数目的黑节点则,因此在删除之后需要判断当前节点是否是黑节点如果是则可能违反红黑树的规定,需要调用
//fixAfterDeletion执行删除后处理保证删除节点后键值对的存储结构仍然是红黑树
if (p.color == BLACK)
fixAfterDeletion(p);
//如果若被删除节点的双亲节点不为空,断开被删除节点与双亲节点的连接
if (p.parent != null) {
//若被删除节点是双亲节点的左孩子,让被删除节点的左孩子指向null
if (p == p.parent.left)
p.parent.left = null;
//若被删除节点是双亲节点的右孩子则让被删除节点的右孩子指向null
else if (p == p.parent.right)
p.parent.right = null;
//被删除节点的双亲节点指向null
p.parent = null;
}
}
}
分析当前方法结合之前的调用方方法源码我们可以大概整理出remove方法的基本逻辑如下:
1)根据key遍历红黑树获取待删除节点,若节点为空返回null;
2)若待删除节点不为空,调用deleteEntry删除节点,若待删除节点的左右孩子节点都存在则让待删除节点的后继节点替代待删除节点,对替换之后的待删除节点分情况进行处理:
选择待删除节点的左孩子节点或右孩子节点为替代节点replacement,优先选择左孩子若左孩子不存在选择右孩子节点。
1.若替代节点replacement不为null。若待删除节点为根节点时,重置根节点为替换节点replacement;若待删除节点为双亲节点的左孩子,则让待删除节点的左孩子指向替代节点replacement,否则让它的双亲节点的右孩子指向替代节点replacement;
之后断开待删除节点与其他节点的连接释放该节点,如果删除的节点是黑色BLACK则还需要调用fixAfterDeletion方法进行节点删除后调整,保证红黑树结构不被破坏;
2.若待删除节点p是根节点,根节点置空;
3.若待删除节点不是根节点且左右子树均为空。若被删除节点颜色是黑色BLACK,因为红黑树必须保证,任意一个叶子节点到根节点的路径中包含黑色节点的个数相同,所以若被删除节点是根节点还需要调用fixAfterDeletion进行节点删除调整,断开与双亲节点的连接。