java进阶|TreeMap源码分析

java进阶|TreeMap源码分析_第1张图片

TreeMap是不是没用过?是的,我也没用过,但是我还是来进行分析它的方法了,因为我要了解一下这个键值对集合的方法有哪些?顺便思考一下它,因为我已经分析过TreeSet这种集合的源码了,如果TreeMap这种键值对集合不分析也不符合我的思考方式,其实对于键值对这种集合的使用场景还是蛮多的这里自己不会说具体的业务是如何做的,但是你可以自己思考思考,所以这里就说到这里,接下来的内容就是TreeMap源码的分析了。

首先先看下TreeMap的构造函数,然后说明一下就可以了。

public TreeMap() {
        comparator = null;
    }

构造一个空参构造函数,与此同时,初始化一下比较器为默认的比较值,这也是TreeMap这种数据结构的特点的。

键值对容器集合构造好了,然后就是装载数据元素了,由于TreeMap不支持键key为null的情况,所以在使用的过程中需要考虑一下这个用法,别自己误操作了导致一些问题出现,得不偿失。

这里的put方法内容比较长,所以需要一点耐心才能看完,看完未必完全理解,但是不要担心,你只需要去看

public V put(K key, V value) {
        Entry t = root;//首先获取根节点,然后将根节点赋值给局部变量t
        if (t == null) {//若根节点为null,说明这是一颗空树,即不含任何节点数据的红黑树
            compare(key, key); // type (and possibly null) check


            root = new Entry<>(key, value, null);
            size = 1;//这个就是将size值置为1
            modCount++;//被修改的次数加1
            return null;
        }
        int cmp;
        Entry parent;
        // split comparator and comparable paths
        Comparator cpr = comparator;//获取比较器,不理解比较器的可以看下我的历史文章
        if (cpr != null) {
            do {
                parent = t;//将根节点赋值给局部变量parent
                cmp = cpr.compare(key, t.key);//将待设置的key和根节点的key进行比较,根据cmp的值进行判断
                //需要操作左子树还是右子树
                if (cmp < 0)//若小于0则操作左子树
                    t = t.left;
                else if (cmp > 0)//若大于0则操作右子树
                    t = t.right;
                else
                    return t.setValue(value);//将待插入的数据value设置到指定节点的位置
            } while (t != null);//这里用到了do...while循环只是为了递归找到待插入的节点位置
        }
        else {
        //走到这里,说明这是一颗非空的数,所以判断待插入的key是否为空
        //若为空则直接抛出NPE异常,其实我个人觉得这个校验可以放到前面做前置校验
        //不知道原作者是什么意图,这里自己没有领会到作者的含义
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable k = (Comparable) key;
            do {
                parent = t;//将根节点赋值给局部变量parent
                cmp = k.compareTo(t.key);//比较待插入的key和根节点的key之间的大小
                if (cmp < 0)//若小于0则去左子树操作数据
                    t = t.left;
                else if (cmp > 0)//若大于0则去右子树操作数据
                    t = t.right;
                else
                    return t.setValue(value);//将待插入的数据插入到指定节点
            } while (t != null);//同样这里用到了do...while循环
        }
        //构造一个entry节点
        Entry e = new Entry<>(key, value, parent);
        if (cmp < 0)//将entry节点设置到指定节点的左子树位置
            parent.left = e;
        else
            parent.right = e;//将entry节点设置到指定节点的右子树位置
        fixAfterInsertion(e);
        size++;//表示map的键值对值size加一
        modCount++;//这句话就是表示数据结构被修改的次数进行加一操作
        return null;
    }
  步骤一:
   /**
     * The number of entries in the tree
     */
    private transient int size = 0;//看到这句化的意思了吧,就是树中entry节点数量的表示
 步骤二:
 
    /**
     * The number of structural modifications to the tree.
     */
    private transient int modCount = 0;//这句化就是数结构被修改的次数的表示

这里简单说下transient这个关键字是什么意思吧,不然自己老是忘了去分析,其实它的意思就是在序列化数据时不会把这部分数据进行存储的,这个示例程序总是没有去写,所以这里就简单提下吧,因为是个小技术点,所以总是忘,每次看到transient关键字自己都会告诉自己要不要去写,结果就是忘了。

好了,上面的put()方法分析完了,不知道你理解了吗?当你和面试官扯皮,不,和面试官交谈时就可以将自己分析的过程和他交流一下,说明他会看中你的口才,不,你的扯皮能力,这里就幽默说下,不要过于认真,这也是自己文章的一点幽默而已。

与以往的文章分析不同,这里就开始分析了isEmpty()方法了,那就先看下这个方法吧。

 public boolean isEmpty() {
        return size() == 0;
    }
 步骤一:
 public int size() {
 //这里主要获取entrySet的个数
        return entrySet().size();
    }
 步骤二:
 public Set> entrySet() {
        EntrySet es = entrySet;//将TreeMap中的entrySet引用赋值给局部变量es
        return (es != null) ? es : (entrySet = new EntrySet());//若es为null,则new出来一个空entrySet进行返回
    } 

键值对集合和其它普通集合不同的一点是它包含了键key以及值value的操作,所以在判断是否包含某个元素时我们可以去操作containsKey()方法以及containsValue()方法。接下来就先看下containsKey()方法吧,若key存在则直接返回true,若不存在则返回false。

public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }
 步骤一:
final Entry getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            return getEntryUsingComparator(key);
           //若key为null,直接抛出NPE异常,所以key不要设置为null,这在put的时候已经校验了,所以这里就加了一下,算是对方法的健壮性做了一种安全策略吧
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable k = (Comparable) key;
        Entry p = root;//将根节点root赋值给局部变量p
        while (p != null) {
        //比较key和根节点的key的大小,若小于0则去左子树去查找,若大于0则去右子树去查找
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;//找到指定节点,则直接返回
        }
        return null;//若查找不到指定节点,则直接返回null,然后进行判断,这样就达到了待查找的key是否存在红黑树中
    } 

由于这里都是将方法的每一句是什么含义在代码中进行了说明,所以这里就没有在文中在过多的去说明了,这样的文章不知道你看的还是习惯吗,我写到这时觉得还好,自己喜欢讲每一句解释的明白,这样我就能和别人交流时进行说说,不,是扯皮时,哈哈。

接下来就是看下containsValue()方法的分析了,到这里又到了深夜,是的,我还在分析这篇文章,我想明天再继续分析吧,但是我又觉得思路若断了,后面再继续捡起时会不会费劲了一点呢?索性这里就继续分析了下去。

public boolean containsValue(Object value) {
        for (Entry e = getFirstEntry(); e != null; e = successor(e))
            if (valEquals(value, e.value))
                return true;
        return false;
    }
步骤一:首先获取第一个entry结点final Entry getFirstEntry() {
        Entry p = root;
        if (p != null)
            while (p.left != null)
                p = p.left;
        return p;
    }
 步骤二:
 static  TreeMap.Entry successor(Entry t) {
        if (t == null)//首先判断传入的entry结点是否为null,若为null则直接返回null,这样对于上面的循环退出做了条件判断
            return null;
        else if (t.right != null) {//若结点的右子树结点不为null,则在这个结点继续查找左子树上面的结点
            Entry p = t.right;
            while (p.left != null)
                p = p.left;
            return p;
        } else {
        //若结点t的右结点为null,则获取t.parent继续判断
            Entry p = t.parent;
            Entry ch = t;
            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }
   步骤三:比较o1,o2是否相等,若相等则直接返回true,跳出for循环,否则返回false
   static final boolean valEquals(Object o1, Object o2) {
        return (o1==null ? o2==null : o1.equals(o2));
    }

这里说明一点哈,==和equals()方法的不同是一个比较地址值,一个比较的是值,说句话的前提条件是这是针对引用类型来说的,对于基本类型而言不是这样的,想了解==和equals()方法的区别可以查找我的历史文章,因为我特意写过一篇文章,因为曾经的我对这个也有一点模糊,所以就去总结了一篇,看来知识的用途和理解总是在其他方面有所串联呢。

如何获取TreeMap键值对容器里面entry节点的个数呢?这就是需要分析一下size()方法了,这里很简单的,就一条语句。

 public int size() {
        return size;//对,就是直接获取size变量的值即可,因为我们在每一次put时,这个size值就会变化,若是新增则值加一,若是替换则不变
    }

    分析到这里了,发现在分析put()方法时忘了分析fixAfterInsertion()这个方法了,这个方法的作用是用来在插入数据后要调整红黑树结构的,所以这里就需要单独进行分析了,毕竟红黑树是一个很深的内容,特别是它的删除操作。

一般我们用HashMap这样的键值对集合时大部分的操作就是根据传入的key获取对应的value值,

 public V get(Object key) {
        Entry p = getEntry(key);//根据key获取entry节点,然后获取entry节点的value值
        return (p==null ? null : p.value);
    }
 步骤一:
 final Entry getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            return getEntryUsingComparator(key);//这个方法就不分析了,因为它和下面的分析差不多,可能就是比较器不一样而已,仅此而已
        if (key == null)//若key为null,则直接抛出空指针异常
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable k = (Comparable) key;
        Entry p = root;//获取根节点,将其赋值给局部变量p
        while (p != null) {
        //判断根节点的key和传入的值的大小,若小于0则去左子树去查找,若大于0则去右子树去查找
            int cmp = k.compareTo(p.key);
            if (cmp < 0)//获取左子树的节点
                p = p.left;
            else if (cmp > 0)//获取右子树额节点
                p = p.right;
            else
                return p;//返回找到的entry节点
        }
        return null;
    }

    其实TreeMap的其它方法已经在TreeSet源码的分析时已经介绍过了,比如说如何获取第一个entry结点的方法getFirstEntry()以及最后一个entry节点的方法getLastEntry(),以及getFirstKey()和getLastKey()进行第一个结点key和最后一个节点key,这里就不在这里过多的说明了,我在想现在是先将TreeSet源码分析文章先给你们分享一下还是这篇文章提前分享呢,等着吧,都别着急,我想分享的内容还是那句话我一般都会分享自己的技术点,其它的不会分享,因为谈论一件事别人会多了很多谈论,自己心里明白就好。

接下来就继续分享TreeMap的replace方法了,其实这个方法用到的很少,想必读到这里的你也有点累了吧,不放休息一下再来看下下面的方法。

 @Override
    public V replace(K key, V value) {
        Entry p = getEntry(key);//获取entry节点,然后将待更新的值进行替换,返回替换之前的值,这也是一贯其它的集合的做法
        if (p!=null) {
            V oldValue = p.value;
            p.value = value;
            return oldValue;
        }
        return null;
    }

其实去删除一个Map中的某个键key的方法remove()用到的也是比较少的,所以这里就简单分析一下好了,毕竟没有过长的代码需要去一句一句分析。

 public V remove(Object key) {
        Entry p = getEntry(key);
        if (p == null)
            return null;


        V oldValue = p.value;
        deleteEntry(p);//删除entry节点,然后重新构建红黑树,这样的文章分析我觉得还是后面单独拿出来进行分析,因为涉及的内容太过复杂
        return oldValue;
    }

走到这里基本上就到了文章的尾声了,所以最后都会去分析一下键值对集合的clear()方法了。

  /**
     * Removes all of the mappings from this map.
     * The map will be empty after this call returns.
     */
    public void clear() {
        modCount++;
        size = 0;//将表示键值对集合entry节点的个数置为0
        root = null;//将根节点置为null,触发GC机制进行垃圾数据的回收
    }

好了,到这里就结束了,喜欢我的文章的可以给个在看呗

我喜欢分享,你喜欢阅读@WwpwW

你可能感兴趣的:(java进阶|TreeMap源码分析)