Map的单元是对键值对的处理,之前分析过的两种Map,HashMap和LinkedHashMap都是用哈希值去寻找我们想要的键值对,优点是理想情况下O(1)的查找速度。

  那如果我们在一个对查找性能要求不那么高,反而对有序性要求比较高的应用场景呢?

这个时候HashMap就不再适用了,我们需要一种新的Map,在JDK中提供了一个接口:SortedMap,我想分析一下具体的实现中的一种:TreeMap.

HashMap是Key无序的,而TreeMap是Key有序的。

  1. TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
  2. TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。
  3. TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。
  4. TreeMap 实现了Cloneable接口,意味着它能被克隆
  5. TreeMap 实现了java.io.Serializable接口,意味着它支持序列化
  6. TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
  7. TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
  8. TreeMap是非同步的(线程不安全的)。 它的iterator 方法返回的迭代器是fail-fastl的。

源码分析:

1.看一下基本成员:

  1. public class TreeMap<K,V>
  2. extends AbstractMap<K,V>
  3. implements NavigableMap<K,V>, Cloneable, java.io.Serializable
  4. {
  5. private final Comparator<? super K> comparator;
  6. private transient Entry<K,V> root = null;
  7. private transient int size = 0;
  8. private transient int modCount = 0;
  9. public TreeMap() {
  10. comparator = null;
  11. }
  12. public TreeMap(Comparator<? super K> comparator) {
  13. this.comparator = comparator;
  14. }
  15. //后面省略
  16. }

  TreeMap继承了NavigableMap,而NavigableMap继承自SortedMap,为SortedMap添加了搜索选项,NavigableMap有几种方法,分别是不同的比较要求:floorKey是小于等于,ceilingKey是大于等于,lowerKey是小于,higherKey是大于。

  注意初始化的时候,有一个Comparator成员,这是用于维持有序的比较器,当我们想做一个自定义数据结构的TreeMap时,可以重写这个比较器。

2.我们看一下Entry的成员:

  1. static final class Entry<K,V> implements Map.Entry<K,V> {
  2. K key;
  3. V value;
  4. Entry<K,V> left = null;
  5. Entry<K,V> right = null;
  6. Entry<K,V> parent;
  7. boolean color = BLACK;
  8. //后续省略
  9. }

  咦?木有了熟悉了哈希值,多了left,right,parent,这是我们的树结构,最后看到color,明白了:TreeMap是基于红黑树实现的!而且默认的节点颜色是黑色。

  至于红黑树,想必多多少少都听过,这是一种平衡的二叉查找树,是2-3树的一种变体,即拥有二叉查找树的高效查找,拥有2-3树的高效平衡插入能力。

  红黑树巧妙的增加了颜色这个维度对2-3树的树本身进行了降维成了二叉树,这样树的调整不会再如2-3树那么繁琐。

  有的同学看到这里会质疑我,你这个胡说八道,和算法导论里讲的不一样!

  对,CLRS中确实没有这段,这段选自《Algorithms》,我觉得提供了一种有趣的理解思路,所以如果之前只看了CLRS,建议去看一下这本书,互相验证。

  不过为了尊重JDK的作者,后面的还是按照CLRS中的讲解来吧,毕竟在JDK源码的注释中写着:From CLR。

  我们在红黑树中的一切插入和删除后,为了维护树的有序性的动作看起来繁复,但都是为了维护下面几个红黑树的基本性质

  1. 树的节点只有红与黑两种颜色
  2. 根节点为黑色的
  3. 叶子节点为黑色的
  4. 红色节点的字节点必定是黑色的
  5. 从任意一节点出发,到其后继的叶子节点的路径中,黑色节点的数目相同

  红黑树的第4条性质保证了这些路径中的任意一条都不存在连续的红节点,而红黑树的第5条性质又保证了所有的这些路径上的黑色节点的数目相同。因而最短路径必定是只包含黑色节点的路径,而最长路径为红黑节点互相交叉的路径,由于所有的路径的起点必须是黑色的,而红色节点又不能连续存在,因而最长路径的长度为全为黑色节点路径长度的二倍

回到TreeMap本身,看看它的put方法:

  1. public V put(K key, V value) {
  2. Entry<K,V> t = root;
  3. if (t == null) {
  4. compare(key, key); // type (and possibly null) check
  5.  
  6. root = new Entry<>(key, value, null);
  7. size = 1;
  8. modCount++;
  9. return null;
  10. }
  11. int cmp;
  12. Entry<K,V> parent;
  13. // split comparator and comparable paths
  14. Comparator<? super K> cpr = comparator;
  15. if (cpr != null) {
  16. do {
  17. parent = t;
  18. cmp = cpr.compare(key, t.key);
  19. if (cmp < 0)
  20. t = t.left;
  21. else if (cmp > 0)
  22. t = t.right;
  23. else
  24. return t.setValue(value);
  25. } while (t != null);
  26. }
  27. else {
  28. if (key == null)
  29. throw new NullPointerException();
  30. Comparable<? super K> k = (Comparable<? super K>) key;
  31. do {
  32. parent = t;
  33. cmp = k.compareTo(t.key);
  34. if (cmp < 0)
  35. t = t.left;
  36. else if (cmp > 0)
  37. t = t.right;
  38. else
  39. return t.setValue(value);
  40. } while (t != null);
  41. }
  42. Entry<K,V> e = new Entry<>(key, value, parent);
  43. if (cmp < 0)
  44. parent.left = e;
  45. else
  46. parent.right = e;
  47. fixAfterInsertion(e);
  48. size++;
  49. modCount++;
  50. return null;
  51. }

此处就是二叉树的比较查找到合适的位置,然后插入,需要注意的是

(1)先检测root节点是不是null,如果为null,则新插入的节点为root节点。

(2)最好自定义自己的Comparator,否则将会继承原始的比较方法,可能会出现问题

(3)插入的键值不能为null,否则会抛出空指针的异常。

(4)插入新节点后,调用fixAfterInsertion(e)方法来修复红黑树。

看一下get方法,这里会调用getEntry方法,就是二叉查找树的查找:

  1. final Entry<K,V> getEntry(Object key) {
  2. // Offload comparator-based version for sake of performance
  3. if (comparator != null)
  4. return getEntryUsingComparator(key);
  5. if (key == null)
  6. throw new NullPointerException();
  7. Comparable<? super K> k = (Comparable<? super K>) key;
  8. Entry<K,V> p = root;
  9. while (p != null) {
  10. int cmp = k.compareTo(p.key);
  11. if (cmp < 0)
  12. p = p.left;
  13. else if (cmp > 0)
  14. p = p.right;
  15. else
  16. return p;
  17. }
  18. return null;
  19. }

还有一个remove方法,这里最后调用的是deleteEntry()方法,在deleteEntry()方法中最后调用fixAfterDeletion方法来修复树的顺序。

红黑树的删除操作复杂的让人发指,对着CLRS慢慢看吧:

  1. public V remove(Object key) {
  2. Entry<K,V> p = getEntry(key);
  3. if (p == null)
  4. return null;
  5.  
  6. V oldValue = p.value;
  7. deleteEntry(p);
  8. return oldValue;
  9. }
  1. private void deleteEntry(Entry<K,V> p) {
  2. modCount++;
  3. size--;
  4.  
  5. // If strictly internal, copy successor's element to p and then make p
  6. // point to successor.
  7. if (p.left != null && p.right != null) {
  8. Entry<K,V> s = successor(p);
  9. p.key = s.key;
  10. p.value = s.value;
  11. p = s;
  12. } // p has 2 children
  13.  
  14. // Start fixup at replacement node, if it exists.
  15. Entry<K,V> replacement = (p.left != null ? p.left : p.right);
  16.  
  17. if (replacement != null) {
  18. // Link replacement to parent
  19. replacement.parent = p.parent;
  20. if (p.parent == null)
  21. root = replacement;
  22. else if (p == p.parent.left)
  23. p.parent.left = replacement;
  24. else
  25. p.parent.right = replacement;
  26.  
  27. // Null out links so they are OK to use by fixAfterDeletion.
  28. p.left = p.right = p.parent = null;
  29.  
  30. // Fix replacement
  31. if (p.color == BLACK)
  32. fixAfterDeletion(replacement);
  33. } else if (p.parent == null) { // return if we are the only node.
  34. root = null;
  35. } else { // No children. Use self as phantom replacement and unlink.
  36. if (p.color == BLACK)
  37. fixAfterDeletion(p);
  38.  
  39. if (p.parent != null) {
  40. if (p == p.parent.left)
  41. p.parent.left = null;
  42. else if (p == p.parent.right)
  43. p.parent.right = null;
  44. p.parent = null;
  45. }
  46. }
  47. }

上面所做的一切繁琐操作都是为了红黑树的基本性质,而修复顺序的操作中最基本的就是左旋和右旋了,下面是左旋和右选的源码。

  1. /** From CLR */
  2. private void rotateLeft(Entry<K,V> p) {
  3. if (p != null) {
  4. Entry<K,V> r = p.right;
  5. p.right = r.left;
  6. if (r.left != null)
  7. r.left.parent = p;
  8. r.parent = p.parent;
  9. if (p.parent == null)
  10. root = r;
  11. else if (p.parent.left == p)
  12. p.parent.left = r;
  13. else
  14. p.parent.right = r;
  15. r.left = p;
  16. p.parent = r;
  17. }
  18. }
  19.  
  20. /** From CLR */
  21. private void rotateRight(Entry<K,V> p) {
  22. if (p != null) {
  23. Entry<K,V> l = p.left;
  24. p.left = l.right;
  25. if (l.right != null) l.right.parent = p;
  26. l.parent = p.parent;
  27. if (p.parent == null)
  28. root = l;
  29. else if (p.parent.right == p)
  30. p.parent.right = l;
  31. else p.parent.left = l;
  32. l.right = p;
  33. p.parent = l;
  34. }
  35. }

其实所有的操作都是关于红黑树的操作,

决定了TreeMap的有序性,对于TreeMap的增删改查的效率都是O(Log(n))的。

参见:

https://www.cnblogs.com/yueyanglou/p/5283915.html

https://www.cnblogs.com/skywang12345/p/3310928.html

java源码之TreeMap的更多相关文章

  1. 【数据结构】11.java源码关于TreeMap

    目录 1.TreehMap的内部结构 2.TreehMap构造函数 3.元素新增策略 4.元素删除 5.元素修改和查找 6.特殊操作 7.扩容 8.总结 1.TreeMap的内部结构 首先确认一点,t ...

  2. java源码 -- TreeMap

    简介 TreeMap 是一个有序的key-value集合,它是通过红黑树实现的.TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合.TreeMap 实现了N ...

  3. 如何阅读Java源码 阅读java的真实体会

    刚才在论坛不经意间,看到有关源码阅读的帖子.回想自己前几年,阅读源码那种兴奋和成就感(1),不禁又有一种激动. 源码阅读,我觉得最核心有三点:技术基础+强烈的求知欲+耐心.   说到技术基础,我打个比 ...

  4. 如何阅读Java源码

    刚才在论坛不经意间,看到有关源码阅读的帖子.回想自己前几年,阅读源码那种兴奋和成就感(1),不禁又有一种激动.源码阅读,我觉得最核心有三点:技术基础+强烈的求知欲+耐心. 说到技术基础,我打个比方吧, ...

  5. Java 源码学习线路————_先JDK工具包集合_再core包,也就是String、StringBuffer等_Java IO类库

    http://www.iteye.com/topic/1113732 原则网址 Java源码初接触 如果你进行过一年左右的开发,喜欢用eclipse的debug功能.好了,你现在就有阅读源码的技术基础 ...

  6. [收藏] Java源码阅读的真实体会

    收藏自http://www.iteye.com/topic/1113732 刚才在论坛不经意间,看到有关源码阅读的帖子.回想自己前几年,阅读源码那种兴奋和成就感(1),不禁又有一种激动. 源码阅读,我 ...

  7. 【java集合框架源码剖析系列】java源码剖析之TreeSet

    本博客将从源码的角度带领大家学习TreeSet相关的知识. 一TreeSet类的定义: public class TreeSet<E> extends AbstractSet<E&g ...

  8. 如何阅读Java源码?

    阅读本文大概需要 3.6 分钟. 阅读Java源码的前提条件: 1.技术基础 在阅读源码之前,我们要有一定程度的技术基础的支持. 假如你从来都没有学过Java,也没有其它编程语言的基础,上来就啃< ...

  9. Java源码阅读的真实体会(一种学习思路)

    Java源码阅读的真实体会(一种学习思路) 刚才在论坛不经意间,看到有关源码阅读的帖子.回想自己前几年,阅读源码那种兴奋和成就感(1),不禁又有一种激动. 源码阅读,我觉得最核心有三点:技术基础+强烈 ...

随机推荐

  1. Callable与Futrue创建线程

    接口callable <V>  类型参数  V-call方法的结构类型 public interface Callable<V> 返回结果并且可能抛出的异常的任务.实现者定义一 ...

  2. CDH版本hadoop2.6伪分布式安装

    1.基础环境配置 主机名 IP地址 角色 Hadoop用户 centos05 192.168.48.105 NameNode.ResourceManager.SecondaryNameNode. Da ...

  3. C#中的public protected internal private

    1.常见的四种方位修饰符关系下图中的protected internal是并集的关系,意思是在命名空间内是internal的,在命名空间外是protected的 2.sealed final seal ...

  4. 导入项目后下载jar包问题理解

    1.导入,然后你下载的jar包,jar下载成功,并不代表你项目里的代码就能用这个jar包了. 你必须还要本地添加进classpath.必须手动添加进类路径 2.总结就是:下载jar包是下载jar包,添 ...

  5. [using_microsoft_infopath_2010]Chapter7 从SharePoint列表和商业连通服务取数据

    本章概要: 1.SharePoint2010列表作为数据源 2.从SharePoint中集中的数据连接库维护数据中获益 3.配置使用外部的项目选择器控件和商业联通服务从外部系统增加信息到你的表单中

  6. HDU 4351

    求某区间各个子集的和的根的最大的五个数. 子集根其实就是这个数模9后的余数,注意的是要区分0和9,两者的余数都是0.. 记录前i个数的和的根,设为sum数组,然后处理两个数组 lft[i][j]表示从 ...

  7. iOS中的crash防护(二)KVC造成的crash

      接上篇< iOS中的crash防护(一)unrecognized selector sent to instance> 我们攻克了找不到方法实现的crash,这一篇我这里主要分析一下在 ...

  8. jQuery Mobile页面跳转切换的几种方式

    jQuery Mobile在移动开发中越来越受到欢迎. 而他的各个版本号也在持续不断的更新中.相同的我也非常喜欢它,它加快了我们开发HTML5的速度. 同一时候又具备jQuery一样的操作方法. 学起 ...

  9. IntelliJ IDEA 10 配置 Tomcat7

    http://blog.csdn.net/neutrojan/article/details/8064145

  10. intellij idea 运行jedis

    到这里下载 http://mvnrepository.com/ jar包! 将jar包放入项目目录中,并引入! 引入包到项目中!创建对象! package com.company; import re ...