TreeMap源码分析

Posted wuqinglong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TreeMap源码分析相关的知识,希望对你有一定的参考价值。

目录

概述

通常情况下存储键值对仅需要HashMap即可满足需求, 但是HashMap有些许缺点, 比如: 1. 不能保证元素的顺序, 因此产生了LinkedHashMap; 2. 不能对元素进行排序, 因此产生了本文中的TreeMap.

TreeMap实现了SortedMap接口, SortedMap接口中定义了排序机制, 以及或者头元素或者尾元素等方法.

本文不会对每个方法进行说明, 只是捡重要的方法说明, 其它方法相信你看完本文就很容易懂的.

存储结构

TreeMap虽然存储了键值对, 看表面与HashMap没什么区别, 但是内部存储结构却截然相反.

存储结构
HashMap 数组+链表/红黑树
TreeMap 红黑树

操作

添加

在看源码之前, 我们可以想一下, 对于排序二叉树的插入操作是如何进行的?

从根开始比较, 如果新元素比树中节点元素小, 就去与节点的左孩子比较大小, 如果新元素比树中节点元素大, 就去与节点的右孩子比较大小, 那么如果相同呢? 与HashMap中的处理一样, 替换其value值.

源码如下:

public V put(K key, V value) {

    // 获取根节点
    Entry<K,V> t = root;

    // 根节点为null --空树
    if (t == null) {

        // 检查key是否为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;

    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator; // 得到比较器

    // 这里根据比较器是否为null进行了区分
    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); // 相等则替换其value值
        } while (t != null);
    }
    else {
        if (key == null)
            throw new NullPointerException();

        // key如果没有实现Comparable接口, 那么这里就会报错
        @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);
    }

    // 建立新节点, 如果小, 就链接到其左节点上, 如果大, 就链接到右节点上
    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;
}

上面代码只是元素的插入代码, 插入新元素之后, 可能会破坏红黑树的性质, 红黑树的调整操作参考另一片文章: 大战红黑树, 也就是上面代码中的fixAfterInsertion(e);方法.

删除

删除操作需要注意一下, 如果删除的节点有两个孩子节点, 则不能直接删除节点, 而是需要找到其前驱或者后继节点来替换该节点, 然后把前驱或者后继节点删除掉即可.

private void deleteEntry(Entry<K,V> p) {
    modCount++;
    size--;

    // 有两个孩子节点, 这里是去寻找后继节点(也就是中根遍历时该节点的后一个节点)
    // 最后将待删除的节点p指向后继节点s
    // If strictly internal, copy successor‘s element to p and then make p
    // point to successor.
    if (p.left != null && p.right != null) {
        Entry<K,V> s = successor(p);
        p.key = s.key;
        p.value = s.value;
        p = s;
    } // p has 2 children

    // 判断待删除节点是否有孩子节点
    // Start fixup at replacement node, if it exists.
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);

    if (replacement != null) { // 有孩子节点

        // 链接孩子节点到父节点上
        // Link replacement to parent
        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;

        // Null out links so they are OK to use by fixAfterDeletion.
        p.left = p.right = p.parent = null;

        // Fix replacement
        if (p.color == BLACK) // 如果删除的是黑节点, 则需要进行红黑树的调整
            fixAfterDeletion(replacement);
    } else if (p.parent == null) { // return if we are the only node.
        root = null;
    } else { //  No children. Use self as phantom replacement and unlink.

        if (p.color == BLACK) // 如果删除的是黑节点, 则需要进行红黑树的调整
            fixAfterDeletion(p);

        // 删除的是红色节点, 进行null操作的赋值
        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(p);方法.

遍历

TreeMap的遍历操作实际上就是二叉树的中序遍历操作.

遍历操作时, 最重要的内部类就是EntryIterator, EntryIterator继承PrivateEntryIterator, 重要的实现在PrivateEntryIterator中, 首先看到PrivateEntryIterator的构造方法需要传入first元素, 也就是头元素, 如何获取头元素呢? TreeMap使用了getFirstEntry()方法, 之后获取下一个节点的时候使用寻找后继的方法进行获取即可.

懂了TreeMap中Entry的遍历, 那么key或者value的遍历自然就知晓了.

扩容

TreeMap不涉及扩容操作, 因为它使用树结构进行存储.

总结

TreeMap除去红黑树的调整之后, 就是非常简单的二叉排序树代码, 学习时我们可以对其进行分离学习.

以上是关于TreeMap源码分析的主要内容,如果未能解决你的问题,请参考以下文章

JDK1.8源码分析之TreeMap

TreeMap 源码分析

源码分析TreeMap和TreeSet

TreeMap源码分析

TreeMap源码分析,看了都说好

死磕 java集合之TreeMap源码分析