TreeMap源码剖析
前言
本文不打算延续前几篇的风格(对所有的源码加入注释),因为要理解透TreeMap的所有源码,对博主来说,确实需要耗费大量的时间和经历,目前看来不大可能有这么多时间的投入,故这里意在通过于阅读源码对TreeMap有个宏观上的把握,并就其中一些方法的实现做比较深入的分析。
红黑树简介
TreeMap是基于红黑树实现的,这里只对红黑树做个简单的介绍,红黑树是一种特殊的二叉排序树,关于二叉排序树,参见:http://blog.csdn.net/ns_code/article/details/19823463,红黑树通过一些限制,使其不会出现二叉树排序树中极端的一边倒的情况,相对二叉排序树而言,这自然提高了查询的效率。
二叉排序树的基本性质如下:
1、每个节点都只能是红色或者黑色
2、根节点是黑色
3、每个叶节点(NIL节点,空节点)是黑色的。
4、如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
5、从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
正是这些性质的限制,使得红黑树中任一节点到其子孙叶子节点的最长路径不会长于最短路径的2倍,因此它是一种接近平衡的二叉树。
说到红黑树,自然不免要和AVL树对比一番。相比较而言,AVL树是严格的平衡二叉树,而红黑树不算严格意义上的平衡二叉树,只是接近平衡,不会让树的高度如BST极端情况那样等于节点的个数。其实能用到红黑树的地方,也都可以用AVL树来实现,但红黑树的应用却非常广泛,而AVL树则很少被使用。在执行插入、删除操作时,AVL树需要调整的次数一般要比红黑树多(红黑树的旋转调整最多只需三次),效率相对较低,且红黑树的统计性能较AVL树要好,当然AVL树在查询效率上可能更胜一筹,但实际上也高不了多少。
红黑树的插入删除操作很简单,就是单纯的二叉排序树的插入删除操作。红黑树被认为比较变态的地方自然在于插入删除后对红黑树的调整操作(旋转和着色),主要是情况分的很多,限于篇幅及博主的熟悉程度优先,这里不打算详细介绍插入删除后调整红黑树的各种情况及其实现,我们有个宏观上的了解即可,如须详细了解,参见算法导论或一些相关的资料。
TreeMap源码剖析
存储结构
TreeMap的排序是基于对key的排序实现的,它的每一个Entry代表红黑树的一个节点,Entry的数据结构如下:
- static final class Entry implements Map.Entry {
-
- K key;
-
- V value;
-
- Entry left = null;
-
- Entry right = null;
-
- Entry parent;
-
- boolean color = BLACK;
-
-
- Entry(K key, V value, Entry parent) {
- this.key = key;
- this.value = value;
- this.parent = parent;
- }
-
- 。。。。。
- }
构造方法
先来看下TreeMap的构造方法。TreeMap一共有4个构造方法。
1、无参构造方法
- public TreeMap() {
- comparator = null;
- }
采用无参构造方法,不指定比较器,这时候,排序的实现要依赖key.compareTo()方法,因此key必须实现Comparable接口,并覆写其中的compareTo方法。
2、带有比较器的构造方法
- public TreeMap(Comparator super K> comparator) {
- this.comparator = comparator;
- }
采用带比较器的构造方法,这时候,排序依赖该比较器,key可以不用实现Comparable接口。
3、带Map的构造方法
- public TreeMap(Map extends K, ? extends V> m) {
- comparator = null;
- putAll(m);
- }
该构造方法同样不指定比较器,调用putAll方法将Map中的所有元素加入到TreeMap中。putAll的源码如下:
-
- public void putAll(Map extends K, ? extends V> map) {
-
- int mapSize = map.size();
-
- if (size==0 && mapSize!=0 && map instanceof SortedMap) {
- Comparator c = ((SortedMap)map).comparator();
-
-
- if (c == comparator || (c != null && c.equals(comparator))) {
- ++modCount;
- try {
- buildFromSorted(mapSize, map.entrySet().iterator(),
- null, null);
- } catch (java.io.IOException cannotHappen) {
- } catch (ClassNotFoundException cannotHappen) {
- }
- return;
- }
- }
-
-
- super.putAll(map);
- }
显然,如果Map里的元素是排好序的,就调用buildFromSorted方法来拷贝Map中的元素,这在下一个构造方法中会重点提及,而如果Map中的元素不是排好序的,就调用AbstractMap的putAll(map)方法,该方法源码如下:
- public void putAll(Map extends K, ? extends V> m) {
- for (Map.Entry extends K, ? extends V> e : m.entrySet())
- put(e.getKey(), e.getValue());
- }
很明显它是将Map中的元素一个个put(插入)到TreeMap中的,主要因为Map中的元素是无序存放的,因此要一个个插入到红黑树中,使其有序存放,并满足红黑树的性质。
4、带有SortedMap的构造方法
- public TreeMap(SortedMapextends V> m) {
- comparator = m.comparator();
- try {
- buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
- } catch (java.io.IOException cannotHappen) {
- } catch (ClassNotFoundException cannotHappen) {
- }
- }
首先将比较器指定为m的比较器,这取决于生成m时调用构造方法是否传入了指定的构造器,而后调用buildFromSorted方法,将SortedMap中的元素插入到TreeMap中,由于SortedMap中的元素师有序的,实际上它是根据SortedMap创建的TreeMap,将SortedMap中对应的元素添加到TreeMap中。
插入删除
插入操作即对应TreeMap的put方法,put操作实际上只需按照二叉排序树的插入步骤来操作即可,插入到指定位置后,再做调整,使其保持红黑树的特性。put源码的实现:
- public V put(K key, V value) {
- Entry t = root;
-
- if (t == null) {
-
-
-
-
-
- root = new Entry(key, value, null);
- size = 1;
- modCount++;
- return null;
- }
- int cmp;
- Entry 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 e = new Entry(key, value, parent);
- if (cmp < 0)
- parent.left = e;
- else
- parent.right = e;
-
- fixAfterInsertion(e);
- size++;
- modCount++;
- return null;
- }
这里的fixAfterInsertion便是节点插入后对树进行调整的方法,这里不做介绍。
删除操作及对应TreeMap的deleteEntry方法,deleteEntry方法同样也只需按照二叉排序树的操作步骤实现即可,删除指定节点后,再对树进行调整即可。deleteEntry方法的实现源码如下:
-
- private void deleteEntry(Entry p) {
- modCount++;
- size--;
-
- if (p.left != null && p.right != null) {
- Entry s = successor (p);
- p.key = s.key;
- p.value = s.value;
- p = s;
- }
-
- Entry replacement = (p.left != null ? p.left : p.right);
-
- if (replacement != null) {
- 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;
-
- p.left = p.right = p.parent = null;
-
- if (p.color == BLACK)
- fixAfterDeletion(replacement);
- } else if (p.parent == null) {
- root = null;
- } else {
- if (p.color == BLACK)
- fixAfterDeletion(p);
-
- if (p.parent != null) {
- if (p == p.parent.left)
- p.parent.left = null;
- else if (p == p.parent.right)
- p.parent.right = null;
- p.parent = null;
- }
- }
- }
后面的fixAfterDeletion方法便是节点删除后对树进行调整的方法,这里不做介绍。
其他很多方法这里不再一一介绍。
几点总结
本文对TreeMap的分析较前几篇文章有些浅尝辄止,TreeMap用的没有HashMap那么多,我们有个宏观上的把我和比较即可。
1、TreeMap是根据key进行排序的,它的排序和定位需要依赖比较器或覆写Comparable接口,也因此不需要key覆写hashCode方法和equals方法,就可以排除掉重复的key,而HashMap的key则需要通过覆写hashCode方法和equals方法来确保没有重复的key。
2、TreeMap的查询、插入、删除效率均没有HashMap高,一般只有要对key排序时才使用TreeMap。
3、TreeMap的key不能为null,而HashMap的key可以为null。
注:对TreeSet和HashSet的源码不再进行剖析,二者分别是基于TreeMap和HashMap实现的,只是对应的节点中只有key,而没有value,因此对TreeMap和HashMap比较了解的话,对TreeSet和HashSet的理解就会非常容易。
LinkedHashmap源码剖析
LinkedHashMap简介
LinkedHashMap是HashMap的子类,与HashMap有着同样的存储结构,但它加入了一个双向链表的头结点,将所有put到LinkedHashmap的节点一一串成了一个双向循环链表,因此它保留了节点插入的顺序,可以使节点的输出顺序与输入顺序相同。
LinkedHashMap可以用来实现LRU算法(这会在下面的源码中进行分析)。
LinkedHashMap同样是非线程安全的,只在单线程环境下使用。
LinkedHashMap源码剖析
LinkedHashMap源码如下(加入了详细的注释):
- package java.util;
- import java.io.*;
-
-
- public class LinkedHashMap
- extends HashMap
- implements Map
- {
-
- private static final long serialVersionUID = 3801124242820219131L;
-
-
-
- private transient Entry header;
-
-
-
-
- private final boolean accessOrder;
-
-
- public LinkedHashMap(int initialCapacity, float loadFactor) {
- super(initialCapacity, loadFactor);
- accessOrder = false;
- }
-
-
- public LinkedHashMap(int initialCapacity) {
- super(initialCapacity);
- accessOrder = false;
- }
-
-
- public LinkedHashMap() {
- super();
- accessOrder = false;
- }
-
-
- public LinkedHashMap(Map extends K, ? extends V> m) {
- super(m);
- accessOrder = false;
- }
-
-
- public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {
- super(initialCapacity, loadFactor);
- this.accessOrder = accessOrder;
- }
-
-
-
-
- void init() {
- header = new Entry(-1, null, null, null);
- header.before = header.after = header;
- }
-
-
-
-
-
-
- void transfer(HashMap.Entry[] newTable) {
- int newCapacity = newTable.length;
- for (Entry e = header.after; e != header; e = e.after) {
- int index = indexFor(e.hash, newCapacity);
- e.next = newTable[index];
- newTable[index] = e;
- }
- }
-
-
-
-
-
- public boolean containsValue(Object value) {
-
- if (value==null) {
- for (Entry e = header.after; e != header; e = e.after)
- if (e.value==null)
- return true;
- } else {
- for (Entry e = header.after; e != header; e = e.after)
- if (value.equals(e.value))
- return true;
- }
- return false;
- }
-
-
-
-
-
-
- public V get(Object key) {
- Entry e = (Entry)getEntry(key);
- if (e == null)
- return null;
- e.recordAccess(this);
- return e.value;
- }
-
-
- public void clear() {
- super.clear();
- header.before = header.after = header;
- }
-
-
- private static class Entry extends HashMap.Entry {
-
- Entry before, after;
-
-
- Entry(int hash, K key, V value, HashMap.Entry next) {
- super(hash, key, value, next);
- }
-
-
- private void remove() {
- before.after = after;
- after.before = before;
- }
-
-
- private void addBefore(Entry existingEntry) {
- after = existingEntry;
- before = existingEntry.before;
- before.after = this;
- after.before = this;
- }
-
-
-
-
-
-
-
-
-
- void recordAccess(HashMap m) {
- LinkedHashMap lm = (LinkedHashMap)m;
-
-
- if (lm.accessOrder) {
- lm.modCount++;
-
- remove();
-
- addBefore(lm.header);
- }
- }
-
- void recordRemoval(HashMap m) {
- remove();
- }
- }
-
-
- private abstract class LinkedHashIterator implements Iterator {
- Entry nextEntry = header.after;
- Entry lastReturned = null;
-
-
-
-
-
-
- int expectedModCount = modCount;
-
- public boolean hasNext() {
- return nextEntry != header;
- }
-
- public void remove() {
- if (lastReturned == null)
- throw new IllegalStateException();
- if (modCount != expectedModCount)
- throw new ConcurrentModificationException();
-
- LinkedHashMap.this.remove(lastReturned.key);
- lastReturned = null;
- expectedModCount = modCount;
- }
-
-
- Entry nextEntry() {
- if (modCount != expectedModCount)
- throw new ConcurrentModificationException();
- if (nextEntry == header)
- throw new NoSuchElementException();
-
- Entry e = lastReturned = nextEntry;
- nextEntry = e.after;
- return e;
- }
- }
-
-
- private class KeyIterator extends LinkedHashIterator {
- public K next() { return nextEntry().getKey(); }
- }
-
-
- private class ValueIterator extends LinkedHashIterator {
- public V next() { return nextEntry().value; }
- }
-
-
- private class EntryIterator extends LinkedHashIterator> {
- public Map.Entry next() { return nextEntry(); }
- }
-
-
- Iterator newKeyIterator() { return new KeyIterator(); }
- Iterator newValueIterator() { return new ValueIterator(); }
- Iterator> newEntryIterator() { return new EntryIterator(); }
-
-
-
-
-
-
- void addEntry(int hash, K key, V value, int bucketIndex) {
-
- createEntry(hash, key, value, bucketIndex);
-
-
- Entry eldest = header.after;
-
-
- if (removeEldestEntry(eldest)) {
- removeEntryForKey(eldest.key);
- } else {
-
- if (size >= threshold)
- resize(2 * table.length);
- }
- }
-
- void createEntry(int hash, K key, V value, int bucketIndex) {
-
- HashMap.Entry old = table[bucketIndex];
- Entry e = new Entry(hash, key, value, old);
- table[bucketIndex] = e;
-
-
-
- e.addBefore(header);
- size++;
- }
-
-
-
-
- protected boolean removeEldestEntry(Map.Entry eldest) {
- return false;
- }
- }
几点总结
关于LinkedHashMap的源码,给出以下几点比较重要的总结:
1、从源码中可以看出,LinkedHashMap中加入了一个head头结点,将所有插入到该LinkedHashMap中的Entry按照插入的先后顺序依次加入到以head为头结点的双向循环链表的尾部。
实际上就是HashMap和LinkedList两个集合类的存储结构的结合。在LinkedHashMapMap中,所有put进来的Entry都保存在如第一个图所示的哈希表中,但它又额外定义了一个以head为头结点的空的双向循环链表,每次put进来Entry,除了将其保存到对哈希表中对应的位置上外,还要将其插入到双向循环链表的尾部。
2、LinkedHashMap由于继承自HashMap,因此它具有HashMap的所有特性,同样允许key和value为null。
3、注意源码中的accessOrder标志位,当它false时,表示双向链表中的元素按照Entry插入LinkedHashMap到中的先后顺序排序,即每次put到LinkedHashMap中的Entry都放在双向链表的尾部,这样遍历双向链表时,Entry的输出顺序便和插入的顺序一致,这也是默认的双向链表的存储顺序;当它为true时,表示双向链表中的元素按照访问的先后顺序排列,可以看到,虽然Entry插入链表的顺序依然是按照其put到LinkedHashMap中的顺序,但put和get方法均有调用recordAccess方法(put方法在key相同,覆盖原有的Entry的情况下调用recordAccess方法),该方法判断accessOrder是否为true,如果是,则将当前访问的Entry(put进来的Entry或get出来的Entry)移到双向链表的尾部(key不相同时,put新Entry时,会调用addEntry,它会调用creatEntry,该方法同样将新插入的元素放入到双向链表的尾部,既符合插入的先后顺序,又符合访问的先后顺序,因为这时该Entry也被访问了),否则,什么也不做。
4、注意构造方法,前四个构造方法都将accessOrder设为false,说明默认是按照插入顺序排序的,而第五个构造方法可以自定义传入的accessOrder的值,因此可以指定双向循环链表中元素的排序规则,一般要用LinkedHashMap实现LRU算法,就要用该构造方法,将accessOrder置为true。
5、LinkedHashMap并没有覆写HashMap中的put方法,而是覆写了put方法中调用的addEntry方法和recordAccess方法,我们回过头来再看下HashMap的put方法:
-
- public V put(K key, V value) {
-
- if (key == null)
- return putForNullKey(value);
-
- int hash = hash(key.hashCode());
- int i = indexFor(hash, table.length);
- for (Entry e = table[i]; e != null; e = e.next) {
- Object k;
-
- if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
-
-
- modCount++;
-
- addEntry(hash, key, value, i);
- return null;
- }
当要put进来的Entry的key在哈希表中已经在存在时,会调用recordAccess方法,当该key不存在时,则会调用addEntry方法将新的Entry插入到对应槽的单链表的头部。
我们先来看recordAccess方法:
-
-
-
-
-
-
-
- void recordAccess(HashMap m) {
- LinkedHashMap lm = (LinkedHashMap)m;
-
-
- if (lm.accessOrder) {
- lm.modCount++;
-
- remove();
-
- addBefore(lm.header);
- }
- }
该方法会判断accessOrder是否为true,如果为true,它会将当前访问的Entry(在这里指put进来的Entry)移动到双向循环链表的尾部,从而实现双向链表中的元素按照访问顺序来排序(最近访问的Entry放到链表的最后,这样多次下来,前面就是最近没有被访问的元素,在实现、LRU算法时,当双向链表中的节点数达到最大值时,将前面的元素删去即可,因为前面的元素是最近最少使用的),否则什么也不做。
再来看addEntry方法:
-
-
-
-
- void addEntry(int hash, K key, V value, int bucketIndex) {
-
- createEntry(hash, key, value, bucketIndex);
-
-
- Entry eldest = header.after;
-
-
- if (removeEldestEntry(eldest)) {
- removeEntryForKey(eldest.key);
- } else {
-
- if (size >= threshold)
- resize(2 * table.length);
- }
- }
-
- void createEntry(int hash, K key, V value, int bucketIndex) {
-
- HashMap.Entry old = table[bucketIndex];
- Entry e = new Entry(hash, key, value, old);
- table[bucketIndex] = e;
-
-
-
- e.addBefore(header);
- size++;
- }
同样是将新的Entry插入到table中对应槽所对应单链表的头结点中,但可以看出,在createEntry中,同样把新put进来的Entry插入到了双向链表的尾部,从插入顺序的层面来说,新的Entry插入到双向链表的尾部,可以实现按照插入的先后顺序来迭代Entry,而从访问顺序的层面来说,新put进来的Entry又是最近访问的Entry,也应该将其放在双向链表的尾部。
上面还有个removeEldestEntry方法,该方法如下:
-
-
-
- protected boolean removeEldestEntry(Map.Entry eldest) {
- return false;
- }
- }
该方法默认返回false,我们一般在用LinkedHashMap实现LRU算法时,要覆写该方法,一般的实现是,当设定的内存(这里指节点个数)达到最大值时,返回true,这样put新的Entry(该Entry的key在哈希表中没有已经存在)时,就会调用removeEntryForKey方法,将最近最少使用的节点删除(head后面的那个节点,实际上是最近没有使用)。
6、LinkedHashMap覆写了HashMap的get方法:
-
-
-
-
- public V get(Object key) {
- Entry e = (Entry)getEntry(key);
- if (e == null)
- return null;
- e.recordAccess(this);
- return e.value;
- }
先取得Entry,如果不为null,一样调用recordAccess方法,上面已经说得很清楚,这里不在多解释了。
7、最后说说LinkedHashMap是如何实现LRU的。首先,当accessOrder为true时,才会开启按访问顺序排序的模式,才能用来实现LRU算法。我们可以看到,无论是put方法还是get方法,都会导致目标Entry成为最近访问的Entry,因此便把该Entry加入到了双向链表的末尾(get方法通过调用recordAccess方法来实现,put方法在覆盖已有key的情况下,也是通过调用recordAccess方法来实现,在插入新的Entry时,则是通过createEntry中的addBefore方法来实现),这样便把最近使用了的Entry放入到了双向链表的后面,多次操作后,双向链表前面的Entry便是最近没有使用的,这样当节点个数满的时候,删除的最前面的Entry(head后面的那个Entry)便是最近最少使用的Entry。
注:本集合源码剖析系列文章转自 http://blog.csdn.net/ns_code 感谢博主!